Site logo
Tác giả
  • avatar Nguyễn Đức Xinh
    Name
    Nguyễn Đức Xinh
    Twitter
Ngày xuất bản
Ngày xuất bản

Tổng hợp đầy đủ các Assertions trong Playwright - Hướng dẫn chi tiết từ cơ bản đến nâng cao

Giới thiệu

Trong bài viết trước về Playwright Assertions và Wait, chúng ta đã tìm hiểu về các khái niệm cơ bản và cách sử dụng assertions trong Playwright. Bài viết này sẽ mở rộng và đi sâu hơn vào tất cả các loại assertions có sẵn trong Playwright, cùng với các ví dụ thực tế và best practices.

Tại sao cần hiểu rõ về Assertions?

Assertions là một phần không thể thiếu trong test automation, giúp chúng ta:

  • Verify rằng ứng dụng hoạt động đúng như mong đợi
  • Phát hiện lỗi và regression sớm
  • Tạo ra test cases chắc chắn và đáng tin cậy
  • Giảm thiểu false positives trong test results

Các loại Assertions trong Playwright

Playwright cung cấp nhiều loại assertions khác nhau để phục vụ các mục đích testing khác nhau:

  1. Page Assertions: Kiểm tra các thuộc tính của trang web như title, URL
  2. Element Assertions: Verify trạng thái và thuộc tính của các elements
  3. Count Assertions: Kiểm tra số lượng elements
  4. Screenshot Assertions: So sánh visual để phát hiện regression
  5. Custom Assertions: Tạo assertions riêng cho business logic cụ thể
  6. Soft Assertions: Cho phép test tiếp tục dù có assertion fail

Trong bài viết này, chúng ta sẽ đi qua từng loại assertion với các ví dụ cụ thể và best practices.

Page Assertions

Page assertions dùng để verify các thuộc tính của trang web:

1. Title Assertions

test('Page title assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app');
  
  // Kiểm tra title chính xác
  await expect(page).toHaveTitle('Home | IT Demo Dummy');
  
  // Kiểm tra title chứa substring
  await expect(page).toHaveTitle(/Demo Dummy/);
  
  // Kiểm tra title không chứa text nào đó
  await expect(page).not.toHaveTitle(/Error/);
});

2. URL Assertions

test('Page URL assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app');
  
  // Kiểm tra URL chính xác
  await expect(page).toHaveURL('https://dummy-demo-njndx.web.app/');
  
  // Kiểm tra URL với regex
  await expect(page).toHaveURL(/dummy-demo-njndx\.web\.app/);
  
  // Navigate và kiểm tra URL thay đổi
  await page.getByTestId('feature-link-todo_list').click();
  await expect(page).toHaveURL(/todo-list/);
  
  // Kiểm tra URL không chứa parameter nào đó
  await expect(page).not.toHaveURL(/debug=true/);
});

3. Network và Response Assertions

test('Network response assertions', async ({ page }) => {
  // Listen for API responses
  const responsePromise = page.waitForResponse('**/api/todos');
  
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  const response = await responsePromise;
  
  // Kiểm tra response status
  expect(response.status()).toBe(200);
  expect(response.ok()).toBeTruthy();
  
  // Kiểm tra response headers
  expect(response.headers()['content-type']).toContain('application/json');
  
  // Kiểm tra response body
  const responseBody = await response.json();
  expect(responseBody).toHaveProperty('todos');
  expect(responseBody.todos).toBeInstanceOf(Array);
});

Element Assertions

Element assertions là nhóm assertions được sử dụng nhiều nhất để verify trạng thái của các elements:

1. Visibility Assertions

test('Element visibility assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra element có visible không
  await expect(page.getByRole('heading', { name: 'Todo List' })).toBeVisible();
  
  // Kiểm tra element bị ẩn
  await expect(page.getByTestId('loading-spinner')).toBeHidden();
  
  // Kiểm tra element attached to DOM
  await expect(page.getByTestId('todo-input')).toBeAttached();
  
  // Kiểm tra element detached from DOM
  await expect(page.getByTestId('deleted-todo')).toBeDetached();
});

2. Content Assertions

test('Element content assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra text content chính xác
  await expect(page.getByRole('heading')).toHaveText('Todo List');
  
  // Kiểm tra text content chứa substring
  await expect(page.getByTestId('welcome-message')).toContainText('Welcome');
  
  // Kiểm tra text content với regex
  await expect(page.getByTestId('todo-count')).toHaveText(/\d+ items/);
  
  // Kiểm tra text content với array (multiple elements)
  await expect(page.locator('.todo-item .title')).toHaveText([
    'Learn Playwright',
    'Write tests',
    'Deploy application'
  ]);
  
  // Kiểm tra inner HTML
  await expect(page.getByTestId('formatted-text')).toContainText('Bold text');
});

3. Form Element Assertions

test('Form element assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/contact');
  
  // Input value assertions
  await page.getByLabel('Email').fill('test@example.com');
  await expect(page.getByLabel('Email')).toHaveValue('test@example.com');
  
  // Checkbox assertions
  await page.getByLabel('Subscribe to newsletter').check();
  await expect(page.getByLabel('Subscribe to newsletter')).toBeChecked();
  
  // Radio button assertions
  await page.getByLabel('Male').check();
  await expect(page.getByLabel('Male')).toBeChecked();
  await expect(page.getByLabel('Female')).not.toBeChecked();
  
  // Select dropdown assertions
  await page.getByLabel('Country').selectOption('Vietnam');
  await expect(page.getByLabel('Country')).toHaveValue('Vietnam');
  
  // File input assertions
  await page.getByLabel('Upload file').setInputFiles('test-file.pdf');
  const fileInput = page.getByLabel('Upload file');
  expect(await fileInput.inputValue()).toContain('test-file.pdf');
});

4. State Assertions

test('Element state assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra element enabled/disabled
  await expect(page.getByTestId('add-button')).toBeEnabled();
  await expect(page.getByTestId('disabled-button')).toBeDisabled();
  
  // Kiểm tra element editable
  await expect(page.getByTestId('todo-input')).toBeEditable();
  await expect(page.getByTestId('readonly-text')).not.toBeEditable();
  
  // Kiểm tra element focused
  await page.getByTestId('todo-input').focus();
  await expect(page.getByTestId('todo-input')).toBeFocused();
  
  // Kiểm tra element empty
  await expect(page.getByTestId('empty-list')).toBeEmpty();
  await expect(page.getByTestId('todo-list')).not.toBeEmpty();
});

5. Attribute Assertions

test('Element attribute assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra attribute có tồn tại
  await expect(page.getByTestId('todo-item')).toHaveAttribute('data-id');
  
  // Kiểm tra attribute value chính xác
  await expect(page.getByTestId('todo-item')).toHaveAttribute('data-id', '123');
  
  // Kiểm tra attribute value với regex
  await expect(page.getByTestId('todo-item')).toHaveAttribute('class', /todo-item/);
  
  // Kiểm tra CSS class
  await expect(page.getByTestId('completed-todo')).toHaveClass('completed');
  await expect(page.getByTestId('important-todo')).toHaveClass(['todo-item', 'important']);
  
  // Kiểm tra CSS properties
  await expect(page.getByTestId('red-text')).toHaveCSS('color', 'rgb(255, 0, 0)');
  await expect(page.getByTestId('large-text')).toHaveCSS('font-size', '24px');
});

Count Assertions

Kiểm tra số lượng elements - rất hữu ích cho dynamic content:

test('Count assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra số lượng chính xác
  await expect(page.locator('.todo-item')).toHaveCount(5);
  
  // Kiểm tra ít nhất
  await expect(page.locator('.todo-item')).toHaveCount({ min: 3 });
  
  // Kiểm tra tối đa
  await expect(page.locator('.todo-item')).toHaveCount({ max: 10 });
  
  // Add new todo và verify count tăng
  await page.getByTestId('todo-input').fill('New todo');
  await page.getByTestId('add-button').click();
  await expect(page.locator('.todo-item')).toHaveCount(6);
  
  // Delete todo và verify count giảm
  await page.locator('.todo-item').first().getByRole('button', { name: 'Delete' }).click();
  await expect(page.locator('.todo-item')).toHaveCount(5);
});

Screenshot Assertions

So sánh visual để detect regression:

test('Screenshot assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Screenshot toàn trang
  await expect(page).toHaveScreenshot('todo-page.png');
  
  // Screenshot một element cụ thể
  await expect(page.getByTestId('todo-list')).toHaveScreenshot('todo-list.png');
  
  // Screenshot với options
  await expect(page).toHaveScreenshot('todo-page-mobile.png', {
    fullPage: true,
    threshold: 0.2, // 20% difference tolerance
    mask: [page.getByTestId('dynamic-timestamp')] // Mask dynamic content
  });
  
  // Screenshot comparison với custom name
  await expect(page.locator('.todo-item').first()).toHaveScreenshot({
    name: 'first-todo-item.png',
    threshold: 0.1
  });
});

Custom Assertions và Matchers

Tạo custom assertions cho business logic cụ thể:

// Custom assertion function
async function expectTodoToBeCompleted(page: Page, todoText: string) {
  const todoItem = page.locator('.todo-item').filter({ hasText: todoText });
  await expect(todoItem).toHaveClass(/completed/);
  await expect(todoItem.getByRole('checkbox')).toBeChecked();
}

// Custom matcher
expect.extend({
  async toHaveValidEmail(locator: Locator) {
    const textContent = await locator.textContent();
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(textContent || '');
    
    return {
      message: () => `Expected ${textContent} to be a valid email`,
      pass
    };
  }
});

test('Custom assertions example', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Sử dụng custom assertion function
  await expectTodoToBeCompleted(page, 'Learn Playwright');
  
  // Sử dụng custom matcher
  await expect(page.getByTestId('user-email')).toHaveValidEmail();
});

Soft Assertions

Cho phép test tiếp tục chạy dù assertion fail:

test('Soft assertions example', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Soft assertions - test sẽ không dừng ngay khi fail
  await expect.soft(page.getByRole('heading')).toHaveText('Todo List');
  await expect.soft(page.locator('.todo-item')).toHaveCount(5);
  await expect.soft(page.getByTestId('add-button')).toBeEnabled();
  
  // Test tiếp tục chạy dù các soft assertion ở trên có thể fail
  await page.getByTestId('todo-input').fill('New todo');
  await page.getByTestId('add-button').click();
  
  // Hard assertion - test sẽ dừng nếu fail
  await expect(page.locator('.todo-item')).toHaveCount(6);
});

Assertion Timeout và Configuration

1. Custom Timeout cho Assertions

test('Custom assertion timeout', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/slow-loading-page');
  
  // Tăng timeout cho assertion cụ thể
  await expect(page.getByTestId('slow-content')).toBeVisible({ timeout: 10000 });
  
  // Giảm timeout cho assertion nhanh
  await expect(page.getByTestId('instant-content')).toBeVisible({ timeout: 1000 });
});

2. Global Assertion Configuration

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Global assertion timeout
  expect: {
    timeout: 10000, // 10 seconds
    toHaveScreenshot: { threshold: 0.2 },
    toMatchSnapshot: { threshold: 0.3 }
  },
  
  use: {
    // Screenshot comparison settings
    screenshot: 'only-on-failure'
  }
});

Debugging Assertions

1. Assertion Error Messages

test('Understanding assertion errors', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  try {
    // Assertion có thể fail
    await expect(page.getByRole('heading')).toHaveText('Wrong Title');
  } catch (error) {
    console.log('Assertion Error:', error.message);
    // Error message sẽ rất chi tiết:
    // Expected string: "Wrong Title"
    // Received string: "Todo List"
  }
});

2. Debug Mode với Assertions

test('Debug assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Log element content trước khi assert
  const headingText = await page.getByRole('heading').textContent();
  console.log('Heading text:', headingText);
  
  // Highlight element trước khi assert
  await page.getByRole('heading').highlight();
  
  // Take screenshot trước assertion quan trọng
  await page.screenshot({ path: 'before-assertion.png' });
  
  await expect(page.getByRole('heading')).toHaveText('Todo List');
});

Advanced Assertion Patterns

1. Conditional Assertions

test('Conditional assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Kiểm tra điều kiện trước khi assert
  const todoCount = await page.locator('.todo-item').count();
  
  if (todoCount > 0) {
    await expect(page.getByTestId('clear-all-button')).toBeVisible();
    await expect(page.getByTestId('todo-counter')).toContainText(`${todoCount} items`);
  } else {
    await expect(page.getByTestId('empty-message')).toBeVisible();
    await expect(page.getByTestId('clear-all-button')).toBeHidden();
  }
});

2. Polling Assertions

test('Polling assertions for dynamic content', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/live-updates');
  
  // Wait for dynamic content to update
  await expect(async () => {
    const liveCount = await page.getByTestId('live-counter').textContent();
    expect(parseInt(liveCount || '0')).toBeGreaterThan(0);
  }).toPass();
  
  // Polling with custom timeout and interval
  await expect(async () => {
    const status = await page.getByTestId('connection-status').textContent();
    expect(status).toBe('Connected');
  }).toPass({
    timeout: 15000,
    intervals: [500, 1000, 2000] // Custom retry intervals
  });
});

3. Multiple Element Assertions

test('Multiple element assertions', async ({ page }) => {
  await page.goto('https://dummy-demo-njndx.web.app/todo-list');
  
  // Assert tất cả todo items có structure đúng
  const todoItems = page.locator('.todo-item');