Danh sách bài viết

Bài 40: test.skip() Chaining — Conditional Skip Nâng Cao

Nhóm A.5 đào sâu test annotations: skip chaining, new signature { tag, annotation } v1.42+, programmatic annotations runtime, tag filter. Bài này mở nhóm bằng cách đi sâu vào các pattern skip phức tạp mà Series 1 bài 402 chưa cover — kết hợp skip với tag và annotation trong cùng khai báo, dùng testInfo.skip() thay vì test.skip() trong body, multiple conditions kết hợp async lookup, và programmatic annotation push lúc runtime.

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

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

Sau khi hoàn thành bài này, bạn sẽ:

  • Dùng được test.skip() chaining với { tag, annotation } object trong cùng một khai báo (v1.42+).
  • Phân biệt test.skip(condition, reason) trong body và testInfo.skip(condition, reason) từ fixture — biết khi nào dùng cái nào.
  • Viết được pattern multiple skip conditions kết hợp async data fetch.
  • Push annotation vào testInfo.annotations lúc runtime và hiểu output trong các reporter.
  • Tránh 4 pitfall điển hình liên quan đến skip nâng cao.
2

Recap Nhanh Từ Series 1 — Và Bài Này Khác Gì

Series 1 bài 402 đã cover 3 hình thức cơ bản:

  • Static skiptest.skip('name', fn) dùng test.skip thay cho test tại khai báo.
  • Conditional skip trong bodytest.skip(condition, reason) bên trong hàm async.
  • Group skiptest.describe.skip(name, fn) skip cả nhóm.

Bài này không lặp lại những phần đó. Thay vào đó, bài tập trung vào:

  • New signature test.skip('name', { tag, annotation }, fn) giới thiệu ở v1.42 — skip, tag, annotate trong 1 dòng khai báo.
  • testInfo.skip() — cách skip qua fixture testInfo, hoạt động ở mọi nơi kể cả beforeEach và custom fixtures.
  • Multiple skip conditions kết hợp async lookup (feature flags, database state...).
  • Programmatic annotation push lúc runtime qua testInfo.annotations.push().

Bài 44 (trong nhóm A.5 này) sẽ đi sâu hơn vào new signature { tag, annotation } với tất cả option. Bài 41 sẽ cover test.fail.only(). Bài này chỉ dùng new signature để minh họa skip chaining.

3

Skip + Tag + Annotation Trong Cùng Khai Báo (v1.42+)

Trước v1.42, muốn skip một test đánh tag attach annotation phải viết ít nhất 2 bước riêng:

// Cách cũ — trước v1.42
test(
  'checkout flow',
  { annotation: { type: 'issue', description: 'JIRA-123' } },
  async ({ page }) => {
    test.skip(true, 'BUG-123 — checkout bị broken');
    // ...
  }
);

Từ v1.42, test.skip chấp nhận cùng options object như test:

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

test.skip(
  'checkout flow',
  {
    tag: '@known-bug',
    annotation: { type: 'issue', description: 'JIRA-123' },
  },
  async ({ page }) => {
    await page.goto('/cart');
    // body không bao giờ chạy, nhưng TypeScript vẫn type-check
  }
);

Kết quả: test này skip tại load time, đồng thời được tag @known-bug (dùng được với --grep) và có annotation issue hiển thị trong HTML report.

TypeScript inference hoạt động đầy đủ — { tag, annotation } object có type TestDetails. Compiler báo lỗi nếu truyền key không hợp lệ.

Tại sao "chaining"?

Tên "skip chaining" xuất phát từ ý tưởng: một test vừa bị skip, vừa mang theo metadata (tag + annotation) như một chain. Trong Playwright source (v1.42+), test.skip, test.fail, test.fixme, test.slow đều nhận cùng overloaded signature — điều này cho phép kết hợp annotation với annotation modifiers một cách đồng nhất.

4

Skip Nhiều Tag Và Nhiều Annotation

Cả tagannotation đều chấp nhận single value hoặc array:

test.skip(
  'payment gateway integration',
  {
    tag: ['@known-bug', '@high-priority'],
    annotation: [
      { type: 'issue', description: 'JIRA-456' },
      { type: 'severity', description: 'P1' },
    ],
  },
  async ({ page }) => {
    await page.goto('/checkout/payment');
    // ...
  }
);

Trong HTML report, mỗi annotation entry hiển thị như một dòng riêng với type làm label. Tag array được flatten thành danh sách, mỗi tag dùng được với --grep độc lập.

Lưu ý: Tag convention của Playwright yêu cầu prefix @ (ví dụ @smoke, @known-bug). Không có prefix, giá trị vẫn được lưu nhưng CLI filter --grep @tag sẽ không match.

5

testInfo.skip() — Skip Qua Fixture

testInfo.skip(condition, reason) là method trên object testInfo — fixture built-in của Playwright Test. Gọi nó có hiệu lực tương đương test.skip(condition, reason) trong body, nhưng truy cập qua testInfo thay vì biến test global.

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

test('feature flag check', async ({ page }, testInfo) => {
  const features = await fetchFeatureFlags(); // async call
  testInfo.skip(!features.darkMode, 'Dark Mode feature not enabled in this env');

  await page.goto('/settings/appearance');
  await expect(page.getByTestId('dark-mode-toggle')).toBeVisible();
});

Khi testInfo.skip() được gọi với condition true, Playwright throw một internal signal để dừng test. Các dòng sau không chạy, fixture teardown vẫn diễn ra bình thường.

Dùng trong custom fixture

testInfo.skip() đặc biệt hữu ích bên trong custom fixture — nơi bạn không thể truy cập biến test global:

// fixtures/db.ts
import { test as base } from '@playwright/test';

export const test = base.extend<{ db: DatabaseClient }>({
  db: async ({}, use, testInfo) => {
    const client = new DatabaseClient();
    const isReady = await client.ping();

    // Skip test ngay nếu DB không sẵn sàng
    testInfo.skip(!isReady, 'Database not reachable — skipping test');

    await use(client);
    await client.close();
  },
});

Trong ví dụ này, bất kỳ test nào dùng fixture db đều tự động skip nếu database không ping được — không cần đặt test.skip() trong từng test riêng lẻ.

6

Phân Biệt test.skip() vs testInfo.skip()

Tiêu chí test.skip(condition, reason) testInfo.skip(condition, reason)
Truy cập qua Biến test import từ @playwright/test Fixture testInfo trong function signature
Dùng được trong test body
Dùng được trong beforeEach
Dùng được trong custom fixture Không khuyến nghị (closure phức tạp) Có — qua tham số testInfo của fixture fn
Kết quả cuối Status skipped Status skipped (giống nhau)
Khi nào nên dùng Trong test body hoặc beforeEach — đơn giản, quen thuộc hơn Trong custom fixture hoặc khi cần truyền qua layer

Về output, hai cách hoàn toàn tương đương — reason đều xuất hiện trong HTML report và JUnit <skipped message="...">. Lựa chọn chỉ là về ergonomics.

testInfo.skip() không có tham số

Tương tự test.skip(), có thể gọi không tham số để skip unconditional:

test('wip test', async ({ page }, testInfo) => {
  testInfo.skip(); // skip ngay, không điều kiện, không lý do
  // ...
});

Bỏ reason là có thể nhưng không nên — xem Pitfall mục 14.

7

Conditional Skip Kết Hợp Browser + OS

Một pattern phổ biến trong CI matrix: skip khi browser X chạy trên OS Y — vì bug chỉ tái hiện ở kết hợp cụ thể đó, không phải browser hay OS riêng lẻ.

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

test.beforeEach(async ({ browserName }, testInfo) => {
  testInfo.skip(
    browserName === 'webkit' && process.platform === 'win32',
    'WebKit trên Windows: file dialog API bị broken — JIRA-789'
  );
});

test('native file picker', async ({ page }) => {
  await page.goto('/upload');
  await page.getByRole('button', { name: 'Choose file' }).click();
  // ...
});

Vì skip được đặt trong beforeEach, mọi test trong block này sẽ bị skip khi đúng điều kiện. Bên ngoài điều kiện đó (WebKit/Linux, Chromium/Windows, v.v.), test chạy bình thường.

Kết hợp nhiều giá trị OS

test('clipboard API', async ({ page, browserName }, testInfo) => {
  const isLinuxWebKit = browserName === 'webkit' && process.platform === 'linux';
  const isWin32Firefox = browserName === 'firefox' && process.platform === 'win32';

  testInfo.skip(
    isLinuxWebKit || isWin32Firefox,
    'Clipboard API không ổn định trên WebKit/Linux và Firefox/Windows'
  );

  await page.goto('/editor');
  await page.keyboard.press('Control+V');
  // ...
});

process.platform trả về 'linux', 'darwin' (macOS), hoặc 'win32' — đây là giá trị Node.js standard, có sẵn không cần import thêm.

8

Multiple Skip Conditions Trong Một Test

Một test có thể có nhiều lý do độc lập để bị skip. Thay vì kết hợp tất cả vào một boolean expression phức tạp, viết từng testInfo.skip() riêng — mỗi cái có reason riêng, dễ đọc và dễ bỏ từng điều kiện khi không còn cần:

test('end-to-end payment flow', async ({ page, browserName }, testInfo) => {
  testInfo.skip(browserName === 'firefox', 'Firefox: payment iframe không load được trên CI');
  testInfo.skip(process.env.SKIP_E2E === 'true', 'E2E test bị tắt trong môi trường này');
  testInfo.skip(process.env.TEST_ENV === 'staging', 'Staging không có payment gateway thực');

  await page.goto('/checkout');
  // ...
});

Playwright dừng tại testInfo.skip() đầu tiên có condition true. Các lệnh sau không evaluate. Reason của condition đó được ghi vào report.

Thứ tự quan trọng: đặt condition có khả năng true cao nhất và rẻ nhất (như env check) lên trước để tránh gọi code đắt (network call, DB query) một cách thừa khi đã biết sẽ skip.

9

Skip Kết Hợp Async Lookup

Một số skip condition cần kết quả từ async call — ví dụ feature flags từ API, trạng thái service health, hoặc dữ liệu seed trong database. Kỹ thuật là await trước rồi truyền kết quả vào testInfo.skip():

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

async function fetchFeatureFlags(): Promise<Record<string, boolean>> {
  const res = await fetch('https://flags.internal/api/flags');
  return res.json();
}

async function isServiceHealthy(service: string): Promise<boolean> {
  try {
    const res = await fetch(`https://health.internal/${service}`);
    return res.ok;
  } catch {
    return false;
  }
}

test('loyalty dashboard', async ({ page }, testInfo) => {
  // Async lookups trước
  const [flags, paymentHealthy] = await Promise.all([
    fetchFeatureFlags(),
    isServiceHealthy('payment'),
  ]);

  testInfo.skip(!flags.loyaltyV2, 'Feature loyaltyV2 chưa được bật trong env này');
  testInfo.skip(!paymentHealthy, 'Payment service không healthy — skip test phụ thuộc');

  await page.goto('/loyalty');
  await expect(page.getByTestId('loyalty-points')).toBeVisible();
});

Dùng Promise.all khi các async call độc lập với nhau để không chờ tuần tự không cần thiết.

Đặt async skip trong beforeAll

Nếu nhiều test trong cùng một describe đều cần cùng async check, tránh gọi lại mỗi test — đặt vào beforeAll và cache kết quả:

test.describe('Loyalty V2 tests', () => {
  let loyaltyEnabled = false;

  test.beforeAll(async () => {
    const flags = await fetchFeatureFlags();
    loyaltyEnabled = flags.loyaltyV2;
  });

  test.beforeEach(async ({}, testInfo) => {
    testInfo.skip(!loyaltyEnabled, 'Feature loyaltyV2 not enabled');
  });

  test('show loyalty points', async ({ page }) => {
    // chỉ chạy nếu loyaltyEnabled = true
  });

  test('redeem loyalty points', async ({ page }) => {
    // chỉ chạy nếu loyaltyEnabled = true
  });
});

Với cách này, fetchFeatureFlags() chỉ được gọi 1 lần cho cả nhóm thay vì N lần.

10

Programmatic Annotation Push Lúc Runtime

testInfo.annotations là mutable array — có thể push annotation vào bất kỳ lúc nào trong quá trình test chạy, kể cả sau khi đã skip. Annotation này sẽ xuất hiện trong HTML report cùng với kết quả test.

test('data export flow', async ({ page }, testInfo) => {
  // Ghi lại environment metadata vào annotation
  testInfo.annotations.push({
    type: 'env',
    description: process.env.TEST_ENV ?? 'unknown',
  });

  const buildId = process.env.CI_BUILD_ID;
  if (buildId) {
    testInfo.annotations.push({
      type: 'ci-build',
      description: buildId,
    });
  }

  await page.goto('/export');
  // ...
});

Dùng case phổ biến hơn — ghi lý do skip động rồi skip:

test('notification flow', async ({ page }, testInfo) => {
  const flags = await fetchFeatureFlags();

  if (!flags.notifications) {
    testInfo.annotations.push({
      type: 'skip-reason',
      description: `Feature 'notifications' off — checked at ${new Date().toISOString()}`,
    });
    testInfo.skip(true, 'Feature notifications not enabled');
  }

  await page.goto('/notifications');
  // ...
});

Khác với reason string trong testInfo.skip(), annotation push cho phép lưu thêm metadata có cấu trúc (type + description) thay vì chỉ plain text. HTML report render annotation như một bảng, dễ đọc hơn khi có nhiều trường.

Push annotation trong fixture teardown

Annotation push không giới hạn ở test body — có thể push trong fixture teardown để ghi lại state sau cleanup:

export const test = base.extend<{ cleanup: void }>({
  cleanup: async ({}, use, testInfo) => {
    await use();
    // Sau khi test chạy xong, ghi lại thông tin cleanup
    testInfo.annotations.push({
      type: 'cleanup',
      description: 'DB seed data removed',
    });
  },
});
11

Skip Trong Reporter — HTML, JUnit, List

HTML Report

Test skip với annotation và tag hiển thị trong HTML report như sau:

  • Status badge màu xám — skipped.
  • Section "Annotations" liệt kê từng annotation entry với type làm label và description làm nội dung.
  • Tag list hiển thị bên cạnh tên test, dùng được để filter trong report UI.
  • Reason từ test.skip(condition, reason) hoặc testInfo.skip(condition, reason) hiển thị dưới dạng annotation type: 'skip' tự động.

JUnit XML

<testcase name="payment gateway integration" classname="checkout.spec.ts" time="0">
  <skipped message="JIRA-456 — Payment service broken"/>
  <properties>
    <property name="issue" value="JIRA-456"/>
    <property name="severity" value="P1"/>
  </properties>
</testcase>

Annotation entries được map thành <property> elements. Các CI tool như Jenkins JUnit plugin đọc được các property này.

List reporter (terminal)

  -  1 [chromium] › checkout.spec.ts:12:5 › payment gateway integration (skipped)
  ✓  2 [chromium] › checkout.spec.ts:28:5 › product listing (passed)

  1 skipped
  1 passed (1.8s)

Reason không hiển thị trong list reporter — chỉ có status. Để xem reason, mở HTML report hoặc dùng --reporter=line verbose.

Tag filter

Skip test với tag vẫn có thể được lọc qua --grep:

# Chỉ chạy (và skip) test có tag @known-bug
npx playwright test --grep @known-bug

# Chạy tất cả, bỏ qua test có tag @known-bug
npx playwright test --grep-invert @known-bug

Chi tiết về tag filter sẽ được cover trong bài sau của nhóm A.5 này.

12

ESM / CJS Edge Case

Một edge case ít gặp nhưng gây ra behavior bất ngờ liên quan đến test.skip() static skip (dạng khai báo) trong ESM module.

ESM top-level await và static skip

Nếu file test dùng ESM (type: "module" trong package.json hoặc file .mts) và có top-level await trước khai báo test:

// CAUTION: ESM với top-level await
const config = await loadConfig(); // top-level await

test.skip('some test', async ({ page }) => {
  // ...
});

Playwright có thể không nhận ra skip tại load time như dự kiến vì module load là async — behavior phụ thuộc vào version runtime. Với CJS (mặc định trong hầu hết Playwright project hiện tại), top-level await không tồn tại nên không có vấn đề này.

Dynamic import trong fixture

Tương tự, nếu fixture dùng dynamic import() để load conditional dependency:

export const test = base.extend({
  specialClient: async ({}, use, testInfo) => {
    // Dynamic import — chỉ load khi cần
    const { SpecialClient } = await import('./special-client.js');
    const client = new SpecialClient();
    const available = await client.isAvailable();

    testInfo.skip(!available, 'SpecialClient service not available');
    await use(client);
    await client.close();
  },
});

Pattern này hoạt động bình thường vì skip được gọi qua testInfo trong async context — không phải load-time static skip.

Kết luận về ESM

Nếu project dùng ESM: ưu tiên dùng testInfo.skip() trong fixture hoặc test body thay vì test.skip() dạng static declaration phụ thuộc vào load-time evaluation. CJS project không có vấn đề này.

13

Limitation

  • Không có audit trail giữa các run: Playwright không tích hợp sẵn cơ chế theo dõi "test này đã skip bao nhiêu run liên tiếp". Để biết skip kéo dài bao lâu, phải parse report history từ CI artifacts bên ngoài.
  • Reason không được enforce: Không có cơ chế bắt buộc truyền reason vào test.skip() hay testInfo.skip(). Cần convention + review để đảm bảo team không bỏ reason.
  • Conditional skip phụ thuộc env không reproducible: Khi skip condition là runtime state (feature flags, service health), reproduce lại behavior "tại sao bài này skip trên CI hôm qua" trở nên khó nếu env đã thay đổi.
  • Annotation trong declaration không có ở report terminal: Annotation push và tag metadata chỉ xuất hiện đầy đủ trong HTML report và JUnit XML. List reporter terminal chỉ hiện status skipped — không có tag, không có annotation details.
14

Pitfall Thường Gặp

1. Bỏ reason khi dùng testInfo.skip() — khó audit sau

// BAD: không có reason
test('flow', async ({ page }, testInfo) => {
  testInfo.skip(!process.env.ENABLE_FEATURE);
  // ...
});

// GOOD: reason rõ ràng
test('flow', async ({ page }, testInfo) => {
  testInfo.skip(!process.env.ENABLE_FEATURE, 'Set ENABLE_FEATURE=1 để chạy test này');
  // ...
});

2. Nhầm test.skip() declaration với testInfo.skip() trong body

// BAD: test.skip() không phải là method của testInfo
test('flow', async ({ page }, testInfo) => {
  testInfo.test.skip(); // TypeError — testInfo không có property test
  // ...
});

// GOOD
test('flow', async ({ page }, testInfo) => {
  testInfo.skip(condition, 'reason'); // đúng
  // hoặc
  test.skip(condition, 'reason'); // cũng đúng, nhưng dùng biến test global
});

3. Đặt testInfo.skip() trong afterEach — không có tác dụng

// BAD: test đã chạy xong, skip không có hiệu lực
test.afterEach(async ({}, testInfo) => {
  if (somePostTestCondition) {
    testInfo.skip(true, 'Try to skip after test'); // quá muộn
  }
});

// GOOD: đặt trong beforeEach hoặc đầu test body
test.beforeEach(async ({}, testInfo) => {
  testInfo.skip(someCondition, 'reason');
});

4. Conditional skip với condition luôn true — tương đương static skip nhưng che giấu intent

// BAD: hardcoded true — nhìn có vẻ conditional nhưng không bao giờ chạy
test('some test', async ({ page }, testInfo) => {
  testInfo.skip(true, 'Temporarily disabled'); // luôn skip
  // ...
});

// GOOD: nếu muốn skip toàn bộ, dùng static skip tại khai báo
test.skip('some test', async ({ page }) => {
  // ...
});
// hoặc gắn annotation rõ ràng:
test.skip(
  'some test',
  { annotation: { type: 'wip', description: 'Not implemented yet' } },
  async ({ page }) => {
    // ...
  }
);
15

Quiz

Câu 1. Khai báo sau đây có hợp lệ với Playwright v1.42+ không? Nếu có, test sẽ xuất hiện như thế nào trong HTML report?

test.skip(
  'payment integration',
  { tag: '@known-bug', annotation: { type: 'issue', description: 'JIRA-100' } },
  async ({ page }) => { ... }
);
Đáp án

Hợp lệ với v1.42+. Test xuất hiện trong HTML report với status skipped, tag @known-bug hiển thị cạnh tên test, và annotation issue: JIRA-100 trong section Annotations. Tag cũng dùng được để filter: --grep @known-bug.

Câu 2. Sự khác biệt về nơi sử dụng giữa test.skip(condition, reason)testInfo.skip(condition, reason) là gì? Khi nào bắt buộc phải dùng testInfo.skip()?

Đáp án

Cả hai đều dùng được trong test body và beforeEach. Tuy nhiên, testInfo.skip() là lựa chọn bắt buộc (hoặc ít nhất là tự nhiên nhất) khi cần skip bên trong custom fixture — nơi bạn không truy cập được biến test global nhưng có testInfo trong function signature của fixture.

Câu 3. Code sau có hoạt động như mong đợi không? Nếu không, vấn đề là gì?

test('async skip test', async ({ page }, testInfo) => {
  const flags = await fetchFeatureFlags();
  testInfo.skip(!flags.newFeature, 'Feature not enabled');
  testInfo.skip(browserName === 'firefox', 'Firefox not supported');
  await page.goto('/new-feature');
});
Đáp án

Code có lỗi TypeScript: browserName không phải fixture tự có trong test signature — phải destructure từ fixture object: async ({ page, browserName }, testInfo). Nếu sửa lại như vậy thì logic hoạt động đúng: dừng tại testInfo.skip() đầu tiên có condition true.

Câu 4. Bạn có nhiều test trong cùng một describe đều cần check feature flag từ API. Cách tốt nhất để tránh gọi API nhiều lần là gì?

Đáp án

Dùng test.beforeAll để gọi API 1 lần và lưu kết quả vào biến outer-scope. Sau đó dùng test.beforeEach với testInfo.skip(!cachedResult, reason) để mỗi test được skip dựa trên giá trị đã cached. Với cách này, N test trong nhóm chỉ trigger 1 API call thay vì N calls.

Câu 5. Khi dùng testInfo.annotations.push({ type: 'env', description: 'staging' }), annotation này xuất hiện ở đâu trong output? Có xuất hiện trong terminal list reporter không?

Đáp án

Annotation push xuất hiện đầy đủ trong HTML report (section Annotations của test) và JUnit XML (dưới dạng <property> element). Terminal list reporter không hiển thị annotation details — chỉ hiện status pass/fail/skip. Để xem annotation trên terminal cần dùng reporter khác (JSON reporter hoặc custom reporter).