Danh sách bài viết

Bài 48: testInfo.tags — Đọc Tag Để Branch Logic

testInfo.tags là read-only array chứa tất cả tag của test hiện tại (bao gồm inherited từ describe). Khác với annotations.push() ở bài 47 — tags không thể thay đổi runtime. Bài này tập trung vào việc đọc tags để điều hướng logic: conditional timeout, conditional skip, conditional fixture, conditional logging — trong beforeEach, fixture teardown, và test body.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này bạn sẽ nắm được:

  • Bản chất read-only của testInfo.tags và tại sao nó khác annotations.push().
  • Cách đọc tags trong beforeEach, test body, và custom fixture để branch logic.
  • 4 pattern thực tế: conditional timeout, conditional skip, conditional logging, fixture branch.
  • Cách tag từ describe được merge vào testInfo.tags.
  • Flexible matching bằng includes()some().
  • Sự khác biệt giữa testInfo.tags runtime branch và --grep pre-run filter.
  • 4 pitfall phổ biến khi dùng tags để branch.
2

testInfo.tags Là Gì

testInfo.tagsstring[] read-only, chứa toàn bộ tag của test đang chạy — bao gồm tag khai báo trực tiếp trên test và tag inherited từ các describe block bao ngoài.

Đây là property được populate trước khi test body bắt đầu, nghĩa là nó có mặt ngay từ dòng đầu tiên của test callback, trong beforeEach, và trong fixture.

Điểm cốt lõi phân biệt với testInfo.annotations:

  • testInfo.tags: read-only, từ khai báo tĩnh ({ tag: [...] }), dùng để đọc và branch logic.
  • testInfo.annotations: mutable array, ghi runtime metadata bằng push(), không ảnh hưởng đến logic test.

Type trong Playwright API (v1.33+):

interface TestInfo {
  readonly tags: string[];
  // ...
}

Keyword readonly là thực sự — runtime push vào array này không có hiệu lực (hoặc throw tùy environment).

3

Cú Pháp Cơ Bản

Truy cập testInfo bằng tham số thứ hai của callback test:

import { test, expect } from '@playwright/test';

test('flow', { tag: ['@smoke', '@critical'] }, async ({ page }, testInfo) => {
  console.log(testInfo.tags);  // ['@smoke', '@critical']

  if (testInfo.tags.includes('@smoke')) {
    // smoke-specific logic
    await page.goto('/quick-check');
  } else {
    await page.goto('/full-flow');
  }
});

Tham số testInfo cũng khả dụng trong các hook và fixture — không chỉ trong test body.

// Trong beforeEach
test.beforeEach(async ({ page }, testInfo) => {
  console.log('Running:', testInfo.title, '| Tags:', testInfo.tags);
});

// Trong custom fixture
const myFixtures = base.extend({
  myService: async ({}, use, testInfo) => {
    console.log('Fixture sees tags:', testInfo.tags);
    await use(someService);
  },
});
4

Pattern: Conditional Timeout

Thay vì set timeout riêng lẻ cho từng test slow, tập trung logic vào beforeEach:

import { test } from '@playwright/test';

test.beforeEach(async ({}, testInfo) => {
  if (testInfo.tags.includes('@slow')) {
    test.setTimeout(120_000);
  }
});

test('quick check', { tag: '@smoke' }, async ({ page }) => {
  // timeout mặc định (30s hoặc theo config)
  await page.goto('/');
});

test('full data export', { tag: ['@regression', '@slow'] }, async ({ page }) => {
  // timeout được set thành 120s bởi beforeEach vì có @slow
  await page.goto('/export');
  await page.getByRole('button', { name: 'Export All' }).click();
  await page.waitForSelector('[data-status="done"]', { timeout: 110_000 });
});

Lợi ích: timeout policy được manage ở 1 chỗ. Thêm test mới chỉ cần gắn @slow, không cần nhớ set timeout thủ công.

Pattern này cũng dùng được để điều chỉnh actionTimeoutnavigationTimeout riêng biệt nếu cần:

test.beforeEach(async ({ page }, testInfo) => {
  if (testInfo.tags.includes('@slow')) {
    test.setTimeout(120_000);
    // Nếu page đã được tạo, có thể set riêng:
    page.setDefaultTimeout(30_000);
    page.setDefaultNavigationTimeout(60_000);
  }
});
5

Pattern: Conditional Skip

Gắn tag mang ý nghĩa "browser constraint" hoặc "environment constraint" rồi skip tập trung ở hook:

test.beforeEach(async ({ browserName }, testInfo) => {
  if (testInfo.tags.includes('@chromium-only') && browserName !== 'chromium') {
    test.skip();
  }
});

test('drag-and-drop', { tag: ['@chromium-only', '@ui'] }, async ({ page }) => {
  // Chỉ chạy trên chromium — bị skip tự động trên firefox và webkit
  await page.goto('/kanban');
  await page.dragAndDrop('[data-card="1"]', '[data-column="done"]');
});

Tương tự, skip theo environment:

test.beforeEach(async ({}, testInfo) => {
  const env = process.env.TEST_ENV ?? 'local';

  if (testInfo.tags.includes('@staging-only') && env !== 'staging') {
    test.skip(true, `Tag @staging-only: skipped on ${env}`);
  }

  if (testInfo.tags.includes('@no-prod') && env === 'production') {
    test.skip(true, 'Tag @no-prod: destructive test, blocked on production');
  }
});

Truyền message vào test.skip(true, reason) sẽ hiển thị trong HTML report — reviewer biết ngay lý do skip mà không cần đọc code.

6

Pattern: Conditional Logging

Test được đánh tag @flaky thường khó debug vì không biết chính xác điều gì thay đổi giữa các lần chạy. Verbose logging giúp thu thập thêm tín hiệu mà không làm chậm toàn bộ suite:

test.beforeEach(async ({ page }, testInfo) => {
  if (testInfo.tags.includes('@flaky')) {
    page.on('console', msg => console.log('PAGE:', msg.text()));
    page.on('request', req => console.log('REQ:', req.url()));
    page.on('response', res => console.log('RES:', res.status(), res.url()));
  }
});

test('payment status poll', { tag: ['@payment', '@flaky'] }, async ({ page }) => {
  // Nếu chạy mà có @flaky → tất cả console, request, response được log
  await page.goto('/payment-status/123');
  await expect(page.getByTestId('status')).toHaveText('confirmed', { timeout: 60_000 });
});

Sau khi test ổn định và bỏ tag @flaky, logging tự tắt mà không cần sửa code trong test body.

Có thể kết hợp với testInfo.attachments để ghi network log ra file đính kèm report:

test.afterEach(async ({}, testInfo) => {
  if (testInfo.tags.includes('@flaky') && testInfo.status !== 'passed') {
    // Đính kèm log vào report khi test fail
    await testInfo.attach('network-log', {
      body: Buffer.from(networkLogs.join('\n')),
      contentType: 'text/plain',
    });
  }
});
7

Pattern: Fixture Branch

Custom fixture có thể đọc testInfo.tags để quyết định provide instance nào:

import { test as base } from '@playwright/test';
import { mockService, realService } from './services';

type Fixtures = {
  apiService: typeof realService;
};

export const test = base.extend<Fixtures>({
  apiService: async ({}, use, testInfo) => {
    if (testInfo.tags.includes('@offline')) {
      await use(mockService);
    } else {
      await use(realService);
    }
  },
});
// Trong spec file
import { test, expect } from './fixtures';

// Dùng mock — không cần network
test('list products offline', { tag: ['@offline', '@smoke'] }, async ({ apiService }) => {
  const products = await apiService.getProducts();
  expect(products).toHaveLength(3); // mock trả về data cố định
});

// Dùng real service
test('list products live', { tag: '@integration' }, async ({ apiService }) => {
  const products = await apiService.getProducts();
  expect(products.length).toBeGreaterThan(0);
});

So với cách truyền flag qua env variable, fixture branch dựa trên tag rõ ràng hơn vì tag visible ngay tại khai báo test — reviewer đọc spec file biết ngay test nào dùng mock.

8

Tag Từ Describe — Merge Behavior

Như đã nói ở bài 45, testInfo.tags chứa tag merged từ tất cả describe bao ngoài cộng với tag của chính test. Điều này có nghĩa là branch logic trong fixture hoặc beforeEach sẽ kích hoạt cho toàn bộ test trong describe mà không cần khai báo lại.

test.describe('Offline Suite', { tag: '@offline' }, () => {

  // Cả 3 test dưới đây đều có @offline trong testInfo.tags
  // → fixture apiService của chúng đều dùng mockService

  test('get products', async ({ apiService }) => {
    const list = await apiService.getProducts();
    expect(list).toHaveLength(3);
  });

  test('get product detail', async ({ apiService }) => {
    const product = await apiService.getProduct('p1');
    expect(product.name).toBe('Mock Product 1');
  });

  test('create order', { tag: '@smoke' }, async ({ apiService }) => {
    // Tags: @offline + @smoke
    const order = await apiService.createOrder({ productId: 'p1' });
    expect(order.id).toBeDefined();
  });

});

Thứ tự trong testInfo.tags không được đảm bảo — không nên dựa vào index. Luôn dùng includes() hoặc some() để kiểm tra sự có mặt của tag.

9

Flexible Matching

Hai cách match tag phổ biến:

Exact Match

// Kiểm tra 1 tag cụ thể
if (testInfo.tags.includes('@smoke')) { ... }

// Kiểm tra nhiều tag — test có bất kỳ tag nào
if (testInfo.tags.some(t => ['@smoke', '@critical'].includes(t))) { ... }

// Kiểm tra test có cả 2 tag
const hasSmoke = testInfo.tags.includes('@smoke');
const hasCritical = testInfo.tags.includes('@critical');
if (hasSmoke && hasCritical) { ... }

Prefix Match

// Match bất kỳ tag nào bắt đầu bằng @p (priority tags: @p0, @p1, @p2)
const hasPriority = testInfo.tags.some(t => t.startsWith('@p'));

// Lấy priority level cụ thể
const priorityTag = testInfo.tags.find(t => t.startsWith('@p'));
const priorityLevel = priorityTag ? parseInt(priorityTag.slice(2)) : null;

// Áp dụng: p0 và p1 được retry thêm
if (priorityLevel !== null && priorityLevel <= 1) {
  // Tăng retry cho test critical
  test.setTimeout(testInfo.timeout * 1.5);
}

Prefix match hữu ích khi taxonomy có nhiều giá trị trong 1 dimension (priority, team, môi trường) và muốn check dimension mà không cần liệt kê từng giá trị.

10

testInfo.tags vs --grep

Hai cơ chế liên quan đến tag nhưng hoạt động hoàn toàn khác nhau:

--grep testInfo.tags
Thời điểm Trước khi chạy (pre-run filter) Trong khi chạy (runtime branch)
Tác động Test không match → không được schedule Mọi test vẫn chạy, code path khác nhau
Mục đích Chọn subset test để chạy Điều chỉnh behavior bên trong test
Ai dùng CLI, CI pipeline Test code, fixture, hook

Ví dụ minh họa sự khác biệt:

# --grep: chỉ test có @slow được chạy, test khác bị bỏ qua hoàn toàn
npx playwright test --grep "@slow"
// testInfo.tags: tất cả test đều chạy
// test nào có @slow thì timeout được tăng lên
test.beforeEach(async ({}, testInfo) => {
  if (testInfo.tags.includes('@slow')) {
    test.setTimeout(120_000);
  }
});

Hai cơ chế có thể kết hợp: dùng --grep "@slow" để chọn đúng subset cần debug, đồng thời testInfo.tags trong beforeEach tự động set timeout phù hợp cho chúng.

11

Combine Fixture Option Và Tag

Fixture option và tag có thể được kết hợp trong cùng 1 fixture để đạt granularity cao hơn:

type Fixtures = {
  variant: 'default' | 'mock';
  apiService: typeof realService;
};

export const test = base.extend<Fixtures>({
  variant: ['default', { option: true }],

  apiService: async ({ variant }, use, testInfo) => {
    // Branch dựa trên cả option lẫn tag
    if (testInfo.tags.includes('@offline') || variant === 'mock') {
      await use(mockService);
    } else {
      await use(realService);
    }
  },
});

Sử dụng:

// Dùng mock vì tag @offline
test('product list offline', { tag: '@offline' }, async ({ apiService }) => {
  // ...
});

// Dùng mock vì option variant=mock (không cần tag)
test.use({ variant: 'mock' });
test('product list mock variant', async ({ apiService }) => {
  // ...
});

// Dùng real (không có @offline, không có variant override)
test('product list live', async ({ apiService }) => {
  // ...
});

Pattern này giữ nguyên tính linh hoạt của fixture option (override per file) trong khi vẫn cho phép per-test override bằng tag.

12

Tags Trong Reporter

Đọc testInfo.tags bên trong test không ảnh hưởng đến cách reporter hiển thị tags. Reporter lấy tag từ khai báo tĩnh — không phụ thuộc vào việc code có đọc tags hay không.

Điều này có nghĩa:

  • Gắn tag @slow để tăng timeout trong beforeEach → tag @slow vẫn hiển thị bình thường trong HTML report như các tag khác.
  • Nếu test skip do testInfo.tags.includes('@chromium-only') → report ghi nhận là skipped, tag vẫn thấy.
  • Tags được đọc hay không đọc, reporter behavior không thay đổi.

Không có cách "ẩn" 1 tag khỏi reporter trong khi vẫn đọc nó ở runtime. Nếu tag chỉ phục vụ internal logic và không muốn xuất hiện trong report, cân nhắc dùng env variable hoặc fixture option thay vì tag.

13

Limitations

  • Read-only: không thể push tag mới vào testInfo.tags runtime. Dynamic metadata phải dùng testInfo.annotations.push().
  • Tag inherit describe không thể override: test con không thể loại bỏ tag của describe cha khỏi testInfo.tags. Nếu describe có @offline, mọi test con đều bị fixture branch về mock dù muốn hay không.
  • Thứ tự không được đảm bảo: testInfo.tags[0] không nhất thiết là tag đầu tiên khai báo. Luôn dùng includes()/some().
  • Không có validation tích hợp: tag typo không được phát hiện — testInfo.tags.includes('@flakey') không match test có tag @flaky, và Playwright không cảnh báo.
14

Pitfalls

Pitfall 1: Tag Typo Gây Silent Skip Branch

// Test khai báo @flaky
test('poll status', { tag: '@flaky' }, async ({ page }, testInfo) => {
  // ...
});

// beforeEach check sai tên tag
test.beforeEach(async ({ page }, testInfo) => {
  if (testInfo.tags.includes('@flakey')) {  // typo: flakey != flaky
    page.on('console', msg => console.log(msg.text()));
  }
  // Log không bao giờ bật → debug infra "hoạt động" nhưng không làm gì
});

Cách phòng: define tag constant thay vì dùng literal string trực tiếp.

// tags.ts
export const Tags = {
  FLAKY: '@flaky',
  SLOW: '@slow',
  OFFLINE: '@offline',
  CHROMIUM_ONLY: '@chromium-only',
} as const;

// Trong test
import { Tags } from './tags';
if (testInfo.tags.includes(Tags.FLAKY)) { ... }

Pitfall 2: Push Tag Runtime Trực Tiếp

// KHÔNG làm — read-only có nghĩa là vậy
testInfo.tags.push('@new-tag');
// Có thể throw TypeError hoặc không có hiệu lực
// Tag không xuất hiện trong report, --grep sau không thấy

// ĐÚNG nếu muốn metadata runtime:
testInfo.annotations.push({ type: 'dynamic', description: 'extra-info' });

Pitfall 3: Confuse tags vs titlePath

// testInfo.tags — chứa tag gắn qua { tag: [...] }
testInfo.tags          // ['@smoke', '@auth']

// testInfo.titlePath — chứa tên file, describe, test theo path
testInfo.titlePath     // ['auth.spec.ts', 'Auth Suite', 'login']

// Không dùng titlePath.includes('@smoke') để check tag
// Không dùng tags.includes('Auth Suite') để check describe name

Pitfall 4: Quên @ Prefix Trong includes Check

// SAI — check 'slow' thay vì '@slow'
if (testInfo.tags.includes('slow')) {
  test.setTimeout(120_000);  // Không bao giờ chạy
}

// ĐÚNG
if (testInfo.tags.includes('@slow')) {
  test.setTimeout(120_000);
}

Tag luôn được lưu với @ prefix trong testInfo.tags. Nếu khai báo tag thiếu @ ({ tag: 'slow' }), thì tag trong array cũng không có @ — và filter CLI sẽ không nhận ra nó.

15

Tổng Kết

  • testInfo.tagsreadonly string[] — đọc được nhưng không thêm được runtime.
  • Khả dụng trong beforeEach, afterEach, test body, và custom fixture.
  • Chứa cả tag khai báo trực tiếp lẫn inherited từ describe — thứ tự không guaranteed.
  • 4 pattern thực tế: conditional timeout (beforeEach), conditional skip (beforeEach + browserName), conditional logging (beforeEach + page events), fixture branch (base.extend).
  • Dùng includes() cho exact match, some(t => t.startsWith(prefix)) cho prefix match.
  • testInfo.tags branch logic IN test — mọi test vẫn chạy. --grep filter BEFORE run — test không match không chạy.
  • Đọc tags không ảnh hưởng reporter — reporter luôn hiển thị tags từ khai báo tĩnh.
  • Tag typo là silent bug — dùng constant object để tránh.
16

Quiz

Câu 1. testInfo.tags.push('@extra') bên trong beforeEach có hoạt động không? Giải thích lý do và nêu cách đúng nếu muốn gắn thêm metadata runtime.

Đáp án

Không hoạt động. testInfo.tagsreadonly — runtime push không có hiệu lực (hoặc throw). Để gắn metadata runtime, dùng testInfo.annotations.push({ type: 'extra', description: '...' }). Annotation xuất hiện trong HTML report nhưng không thể dùng cho --grep.

Câu 2. Test nằm trong test.describe('Suite', { tag: '@offline' }) và được khai báo { tag: '@smoke' }. Giá trị testInfo.tags khi test chạy chứa những gì? Fixture branch theo @offline có kích hoạt không?

Đáp án

testInfo.tags chứa cả @offline lẫn @smoke (thứ tự không guaranteed). Fixture kiểm tra testInfo.tags.includes('@offline') sẽ trả về true → fixture branch về mock sẽ kích hoạt.

Câu 3. Muốn kiểm tra test có bất kỳ priority tag nào (@p0, @p1, @p2, @p3) mà không liệt kê từng giá trị, viết điều kiện nào?

Đáp án
const hasPriority = testInfo.tags.some(t => t.startsWith('@p'));

Hoặc nếu cần giới hạn chỉ @p0@p3 (không match @payment): testInfo.tags.some(t => /^@p\d+$/.test(t)).

Câu 4. Sự khác biệt chính giữa testInfo.tags--grep khi cùng liên quan đến tag là gì?

Đáp án

--grep hoạt động trước khi test chạy — test không match pattern sẽ không được schedule. testInfo.tags hoạt động trong khi test chạy — mọi test đều được chạy, nhưng code path bên trong test có thể khác nhau tùy tag. Hai cơ chế phục vụ mục đích khác nhau và có thể kết hợp.

Câu 5. beforeEach check testInfo.tags.includes('@slow') và gọi test.setTimeout(120_000). Test không có tag @slow nhưng nằm trong describe có tag @slow. Timeout có được set thành 120s không?

Đáp án

Có. testInfo.tags bao gồm cả tag inherited từ describe — test không cần khai báo @slow trực tiếp. Nếu describe cha có { tag: '@slow' }, tất cả test con đều có @slow trong testInfo.tags và điều kiện trong beforeEach sẽ kích hoạt.