Danh sách bài viết

Bài 12: Option Fixture storageState

storageState là Option Fixture — nhận path tới file JSON chứa cookies + localStorage (hoặc object inline) và truyền vào BrowserContext khi tạo, cho phép test bắt đầu ở trạng thái đã xác thực mà không cần chạy qua login UI. Bài này nhìn từ góc fixture: cách configure ở global use:, override per-file, per-describe, multi-role project matrix, reset về guest bằng empty object, và 4 pitfall hay gặp trong môi trường CI.

27/05/2026
14 phút đọc
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ẽ:

  • Hiểu storageState hoạt động ở tầng fixture như thế nào — tại sao nó là Option Fixture, không phải config thụ động trong playwright.config.ts.
  • Configure storageState ở global use:, per-project, per-file, và per-describe đúng cách.
  • Biết hai dạng giá trị: string path tới file JSON và object inline { cookies, origins }.
  • Thiết lập multi-role project matrix (admin / user / guest) sử dụng storageState per-project.
  • Reset về trạng thái guest đúng cách bằng empty object — không phải undefined.
  • Nắm được pattern setup project dependency trên CI (tổng quan, không deep dive).
  • Tránh 4 pitfall phổ biến liên quan file không tồn tại, token hết hạn, worker context conflict, và reset không đúng cách.

Series 1 (bài 307–316) đã cover storageState cơ bản: khái niệm, cú pháp save/load, cấu trúc JSON, folder convention playwright/.auth/. Bài này không lặp lại những nội dung đó — focus vào cách storageState hoạt động với tư cách Option Fixture và các pattern override theo layer.

2

storageState Là Option Fixture — Không Phải Config Thụ Động

Trong Playwright Test, storageState được định nghĩa như một Option Fixture với annotation { option: true }. Điều này khác với một key thông thường trong playwright.config.ts:

// Simplified từ source packages/playwright/src/common/config.ts
storageState: [undefined, { option: true }],

Vì là Option Fixture, storageState tham gia vào fixture pipeline đầy đủ — có thể override ở bất kỳ layer nào qua use:. Và quan trọng hơn, giá trị của nó được context fixture đọc khi gọi browser.newContext():

storageState option fixture
       │
       ▼
context fixture → browser.newContext({ storageState: value })
       │
       ▼
page fixture → context.newPage()
       │
       ▼
Test body bắt đầu — cookies + localStorage đã được load vào context

Vì storageState được đặt vào context lúc khởi tạo, test body không cần làm thêm gì — page.goto('/dashboard') đầu tiên sẽ thấy context đã authenticated. Đây là điểm khác biệt so với login thủ công trong beforeAll: với Option Fixture, không có code setup, không có phụ thuộc thứ tự giữa các test.

Layer ưu tiên từ thấp đến cao:

  1. Global use.storageState trong defineConfig()
  2. Project-level projects[i].use.storageState
  3. File-level test.use({ storageState: ... }) đặt ngoài mọi test()
  4. Describe-level test.use({ storageState: ... }) đặt bên trong test.describe()

Layer càng gần test body càng có độ ưu tiên cao hơn. Project-level bị ghi đè bởi file-level, file-level bị ghi đè bởi describe-level.

3

Configure Global Qua use:

Khi toàn bộ test suite chạy dưới một identity duy nhất (ví dụ: app không có phân quyền phức tạp, mọi test đều login như user), đặt storageState ở global use::

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

export default defineConfig({
  use: {
    storageState: 'playwright/.auth/user.json',
  },
});

Với config này, mọi test trong suite — bất kể project nào, bất kể file nào — đều khởi tạo context với state của user.json. Không cần viết login code trong bất kỳ test nào.

Khi chỉ một phần test cần auth, dùng project-level thay vì global — xem mục 8 (Multi-Role Project Matrix).

4

Hai Dạng Giá Trị: String Path Vs Object Inline

storageState nhận một trong hai dạng giá trị:

String — path tới file JSON

use: {
  storageState: 'playwright/.auth/user.json',
}

Path được resolve từ thư mục playwright.config.ts (cùng quy tắc với testDir). File JSON phải tồn tại tại thời điểm test chạy — nếu không Playwright ném lỗi ENOENT: no such file or directory.

Nội dung file JSON có cấu trúc chuẩn:

{
  "cookies": [
    {
      "name": "session",
      "value": "abc123",
      "domain": "localhost",
      "path": "/",
      "expires": 1800000000,
      "httpOnly": true,
      "secure": false,
      "sameSite": "Lax"
    }
  ],
  "origins": [
    {
      "origin": "http://localhost:3000",
      "localStorage": [
        { "name": "authToken", "value": "eyJhbGci..." },
        { "name": "userId", "value": "42" }
      ]
    }
  ]
}

Cấu trúc JSON chi tiết đã được phân tích trong Series 1 bài 312 — bài này không lặp lại.

Object inline

// Dùng khi muốn set state không có file trên disk
test.use({
  storageState: {
    cookies: [],
    origins: [],
  },
});

Object inline thường dùng để reset về trạng thái guest (không có cookies, không có localStorage) — chi tiết ở mục 7. Cũng có thể inject state nhỏ trực tiếp vào test mà không cần tạo file:

test.use({
  storageState: {
    cookies: [],
    origins: [
      {
        origin: 'http://localhost:3000',
        localStorage: [
          { name: 'featureFlag', value: 'new-ui' },
        ],
      },
    ],
  },
});

Dùng object inline hợp lý khi state nhỏ, không cần chia sẻ giữa nhiều file. Khi state lớn hoặc cần reuse, dùng file JSON.

5

Per-File Override

Khi global config đặt storageStateuser.json, một file test cần chạy với quyền admin có thể override toàn bộ file bằng test.use() đặt ở file-scope (ngoài mọi test()describe()):

// admin.spec.ts
import { test, expect } from '@playwright/test';

// Override cho toàn bộ file này — mọi test trong file dùng admin.json
test.use({ storageState: 'playwright/.auth/admin.json' });

test('admin can delete users', async ({ page }) => {
  await page.goto('/admin/users');
  // Context đã loaded admin.json — đang login như admin
  await page.getByRole('button', { name: 'Delete user' }).first().click();
  await expect(page.getByText('User deleted')).toBeVisible();
});

test('admin can view audit log', async ({ page }) => {
  await page.goto('/admin/audit-log');
  // Vẫn là admin — cùng file-level storageState
  await expect(page.getByRole('table')).toBeVisible();
});

Lưu ý: test.use() bên trong test body không có tác dụng — Option Fixture được đọc trước khi test body chạy, ở giai đoạn fixture resolution:

// SAI — không override được
test('wrong', async ({ page }) => {
  test.use({ storageState: 'playwright/.auth/admin.json' }); // bị bỏ qua
  await page.goto('/admin');
  // context vẫn dùng storageState từ config cũ
});
6

Per-Describe Override — Multi-Role Trong Cùng File

Khi một file test cần kiểm tra hành vi khác nhau theo role, đặt test.use() bên trong từng describe block:

// dashboard-access.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Admin tests', () => {
  test.use({ storageState: 'playwright/.auth/admin.json' });

  test('admin sees management panel', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page.getByRole('link', { name: 'Management' })).toBeVisible();
  });

  test('admin can access settings', async ({ page }) => {
    await page.goto('/dashboard/settings');
    await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
  });
});

test.describe('Regular user tests', () => {
  test.use({ storageState: 'playwright/.auth/user.json' });

  test('user does not see management panel', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page.getByRole('link', { name: 'Management' })).not.toBeVisible();
  });
});

test.describe('Guest tests', () => {
  test.use({ storageState: { cookies: [], origins: [] } });  // empty = no auth

  test('guest is redirected to login', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page).toHaveURL('/login');
  });
});

Pattern trên cho phép test các role khác nhau trong cùng một file mà không cần tách ra nhiều file. Tuy nhiên, cần biết về worker behavior khi dùng per-describe storageState — xem Pitfall 3 trong mục 12.

7

Reset Về Guest: Empty Object Override

Khi global hoặc project config đặt storageState để auth, một số test cần chạy như guest (không có auth). Cách reset về guest:

// Đặt ở file-scope — toàn bộ file chạy như guest
test.use({ storageState: { cookies: [], origins: [] } });

test('login form renders correctly', async ({ page }) => {
  // Bắt đầu không có cookies, không có localStorage auth
  await page.goto('/login');
  await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
});

test('forgot password link works', async ({ page }) => {
  await page.goto('/login');
  await page.getByRole('link', { name: 'Forgot password?' }).click();
  await expect(page).toHaveURL('/forgot-password');
});

Lý do dùng { cookies: [], origins: [] } thay vì undefined:

  • storageState: undefined nghĩa là "dùng giá trị mặc định từ layer trên" — không phải reset. Nếu project config đang set user.json, test dùng undefined vẫn sẽ load user.json.
  • storageState: { cookies: [], origins: [] } là giá trị hợp lệ, ghi đè rõ ràng lên layer trên — context được tạo với state rỗng.
// SAI — không reset về guest, chỉ fallback về default
test.use({ storageState: undefined }); // vẫn dùng user.json từ project config

// ĐÚNG — reset hoàn toàn về guest
test.use({ storageState: { cookies: [], origins: [] } });
8

Multi-Role Project Matrix

Pattern phổ biến trong test suite có phân quyền là định nghĩa một project cho mỗi role, mỗi project dùng storageState riêng. Toàn bộ test của role đó được assign cho project tương ứng qua testMatch:

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

export default defineConfig({
  projects: [
    {
      name: 'admin',
      use: { storageState: 'playwright/.auth/admin.json' },
      testMatch: /admin\..*\.spec\.ts/,
    },
    {
      name: 'user',
      use: { storageState: 'playwright/.auth/user.json' },
      testMatch: /user\..*\.spec\.ts/,
    },
    {
      name: 'guest',
      // Không set storageState → context khởi tạo không có auth
      testMatch: /guest\..*\.spec\.ts/,
    },
  ],
});

File test được đặt tên theo convention role:

tests/
├── admin.users.spec.ts       → chạy với project 'admin'
├── admin.settings.spec.ts    → chạy với project 'admin'
├── user.dashboard.spec.ts    → chạy với project 'user'
├── user.profile.spec.ts      → chạy với project 'user'
└── guest.landing.spec.ts     → chạy với project 'guest'

Ưu điểm của pattern này so với per-describe override:

  • Mỗi project chạy song song độc lập — không có worker conflict.
  • HTML report phân tách rõ theo project name — dễ xác định role nào fail.
  • File test gọn hơn — không cần khai báo test.use() trong mỗi file.

Nhược điểm: khi cần test cross-role trong cùng file (ví dụ: verify admin thấy thứ gì mà user không thấy), vẫn phải dùng per-describe override.

9

Setup Project Dependency — CI Pattern (Tổng Quan)

Trên CI, file playwright/.auth/*.json không tồn tại sẵn — cần được sinh ra trước khi test chính chạy. Pattern phổ biến là dùng setup project với dependencies:

// playwright.config.ts
export default defineConfig({
  projects: [
    // 1. Setup project: login UI → lưu storageState
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },

    // 2. Main project: depend vào setup → load storageState
    {
      name: 'e2e',
      use: { storageState: 'playwright/.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});
// auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate as user', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('[email protected]');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('/dashboard');

  // Lưu state sau khi login thành công
  await page.context().storageState({ path: 'playwright/.auth/user.json' });
});

Setup project chạy một lần trước toàn bộ project phụ thuộc — không phải trước mỗi test. Pattern này phổ biến trong CI workflow vì:

  • Login UI chỉ chạy 1 lần dù có hàng trăm test.
  • File state được commit vào disk, load lại giữa các test mà không cần browser extra.
  • Playwright đảm bảo thứ tự chạy: setup xong mới chạy e2e.

Chương A.6 sẽ deep dive setup project pattern — dependencies matrix, parallel setup cho multi-role, cleanup sau test run. Bài này chỉ cho thấy big picture.

10

Reset State Runtime Với context.clearCookies()

Đôi khi test cần xóa state trong runtime — ví dụ: kiểm tra behavior sau khi logout, hoặc verify redirect khi session cookie bị xóa. Dùng context.clearCookies() và/hoặc context.clearPermissions():

test('session expired → redirect to login', async ({ page, context }) => {
  // context đã load storageState (user đang login)
  await page.goto('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  // Simulate session expire: xóa cookies
  await context.clearCookies();

  // Navigate → expect redirect về login
  await page.goto('/dashboard');
  await expect(page).toHaveURL('/login');
});

Để xóa cả localStorage (token lưu trong origin), cần dùng page.evaluate():

test('logout clears auth state', async ({ page, context }) => {
  await page.goto('/');

  // Xóa toàn bộ auth state
  await context.clearCookies();
  await page.evaluate(() => window.localStorage.clear());

  await page.reload();
  await expect(page).toHaveURL('/login');
});

Khác với Option Fixture (set khi tạo context), clearCookies() là thao tác runtime — ảnh hưởng từ thời điểm gọi trở đi trong cùng context. Các test khác chạy với context mới (do page-scope fixture) không bị ảnh hưởng.

Lưu ý: context.storageState({ path: 'temp.json' }) trả về state hiện tại của context — hữu ích để debug: gọi trước và sau clearCookies để xác nhận state đã thay đổi.

11

IndexedDB Và setStorageState() — Mention Qua

Hai tính năng mới liên quan storageState cần biết để tránh nhầm lẫn, nhưng sẽ được deep dive ở chương B (Authentication nâng cao):

IndexedDB support (v1.51+)

Từ v1.51, Playwright thêm khả năng capture IndexedDB vào storageState. Mặc định, context.storageState() chỉ lưu cookies và localStorage. IndexedDB không tự động được include — phải config riêng khi save.

Bài này không deep dive vì IndexedDB storageState có nhiều edge case (size limit, serialization của binary data, browser compatibility). Chương B sẽ cover đầy đủ.

context.setStorageState() (v1.59+)

Từ v1.59, có API context.setStorageState(state) cho phép đổi state của context đang chạy mà không cần tạo context mới. Hữu ích khi test cần switch identity trong runtime — ví dụ: test flow "admin approve → user receive notification" trong cùng một test.

Chương B sẽ deep dive pattern này. Ở đây chỉ cần biết API tồn tại và không nhầm với context.storageState() (đọc state) vs context.setStorageState() (ghi state).

12

4 Pitfalls Thực Tế

1. File auth.json không tồn tại — test fail với ENOENT

// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }

// Lỗi khi chạy nếu file chưa được tạo:
// Error: ENOENT: no such file or directory, open 'playwright/.auth/user.json'

Nguyên nhân: CI checkout mới, không có file auth. Fix: dùng setup project dependency để sinh file trước khi test chính chạy. Không commit file auth vào git (có chứa credentials).

Nếu cần fallback graceful (chạy mà không có auth khi file không tồn tại), kiểm tra file tồn tại trước:

import { defineConfig } from '@playwright/test';
import { existsSync } from 'fs';

const authFile = 'playwright/.auth/user.json';

export default defineConfig({
  use: {
    storageState: existsSync(authFile) ? authFile : undefined,
  },
});

2. JWT Token trong storageState đã expire

storageState lưu giá trị tĩnh tại thời điểm save. Nếu app dùng JWT với thời hạn ngắn (ví dụ 1 giờ), file state sinh lúc sáng sẽ hết hạn vào chiều:

// Dấu hiệu: test fail với lỗi 401 Unauthorized hoặc redirect về /login
// dù storageState được set đúng

// Không phải bug của Playwright — là token hết hạn
// Fix: refresh file state thường xuyên hơn, hoặc dùng refresh token trong setup script

Với app có JWT expiry ngắn, cần run setup project ở đầu mỗi CI job (không cache state giữa job). Hoặc dùng API login thay vì UI login trong setup script để nhanh hơn.

3. Per-describe storageState override và worker context sharing

storageState là test-scope option (khác với headless là worker-scope). Mỗi test tạo context mới với storageState của describe block đó — không share context giữa describe blocks khác nhau. Tuy nhiên, nếu dùng test.describe.configure({ mode: 'serial' }) với shared context, storageState chỉ được apply lần đầu:

// Cẩn thận với serial mode + shared context
test.describe.configure({ mode: 'serial' });

test.describe('Admin serial tests', () => {
  test.use({ storageState: 'playwright/.auth/admin.json' });
  // OK — storageState apply cho mỗi test riêng (mỗi test có context mới)
});

// Nguy hiểm: khi dùng context fixture trực tiếp với beforeAll
// trong serial mode — context được tạo 1 lần trong beforeAll,
// storageState từ test.use() có thể không apply đúng

Tránh kết hợp test.describe.configure({ mode: 'serial' }) + shared context fixture + per-describe storageState override nếu không hiểu rõ lifecycle.

4. storageState: undefined không reset về guest

// Tình huống: project config set storageState: 'playwright/.auth/user.json'
// Dev muốn một describe block chạy như guest

// SAI — undefined không ghi đè, fallback về project config (user.json)
test.describe('Guest flow', () => {
  test.use({ storageState: undefined }); // không reset!
  test('should redirect to login', async ({ page }) => {
    await page.goto('/dashboard');
    // page.goto('/login') không xảy ra — context vẫn load user.json
    await expect(page).toHaveURL('/dashboard'); // pass sai
  });
});

// ĐÚNG — empty object ghi đè rõ ràng
test.describe('Guest flow', () => {
  test.use({ storageState: { cookies: [], origins: [] } });
  test('should redirect to login', async ({ page }) => {
    await page.goto('/dashboard');
    await expect(page).toHaveURL('/login'); // đúng
  });
});
13

Tổng Kết

  • storageState là Option Fixture — được context fixture đọc khi gọi browser.newContext(). Test body nhận context đã có auth, không cần login code.
  • Nhận hai dạng giá trị: string path tới file JSON hoặc object inline { cookies, origins }.
  • Override per-file: đặt test.use() ở file-scope (ngoài mọi test()). Override per-describe: đặt bên trong test.describe(). Không đặt test.use() bên trong test body.
  • Reset về guest: dùng { cookies: [], origins: [] } — không phải undefined. undefined fallback về layer trên.
  • Multi-role project matrix: một project per role, testMatch theo naming convention. Sạch hơn per-describe khi test không cần cross-role trong cùng file.
  • Setup project dependency: sinh file auth trước khi test chính chạy — pattern chuẩn trên CI. Deep dive ở chương A.6.
  • IndexedDB support từ v1.51 — không auto-include. context.setStorageState() từ v1.59 — đổi state runtime. Cả hai deep dive ở chương B.
  • 4 pitfall chính: file không tồn tại (ENOENT), token expire, serial mode + shared context conflict, và undefined không reset về guest.
14

Quiz Củng Cố

Câu 1

Đoạn config sau có vấn đề gì khi chạy trên CI lần đầu tiên (fresh checkout)?

export default defineConfig({
  use: {
    storageState: 'playwright/.auth/user.json',
  },
});
Đáp án

Trên CI fresh checkout, file playwright/.auth/user.json không tồn tại. Playwright sẽ throw ENOENT: no such file or directory khi cố load storageState. Fix: thêm setup project sinh file trước khi test chính chạy, hoặc dùng existsSync(authFile) ? authFile : undefined để fallback graceful.

Câu 2

Tại sao đoạn code sau không reset context về trạng thái guest, dù dev có ý định như vậy?

// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }

// guest.spec.ts
test.use({ storageState: undefined });
test('should see login page', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL('/login');
});
Đáp án

storageState: undefined trong test.use() không ghi đè giá trị từ layer trên — nó chỉ "không cung cấp giá trị", khiến Playwright fallback về project/global config (user.json). Test vẫn chạy với context đã authenticated. Để reset thực sự về guest, phải dùng empty object: test.use({ storageState: { cookies: [], origins: [] } }).

Câu 3

Khi đặt test.use({ storageState: 'playwright/.auth/admin.json' }) bên trong test.describe(), Playwright apply storageState này như thế nào — cho toàn file hay chỉ cho describe block?

Đáp án

Chỉ áp dụng cho các test bên trong describe block đó. Các test bên ngoài describe block (hoặc trong describe block khác không có test.use()) vẫn dùng storageState từ layer cao hơn (file-level hoặc project/global config). Layer rule: describe-level override file-level, file-level override project-level, project-level override global.

Câu 4

Trong multi-role project matrix, tại sao pattern dùng testMatch per-project ưu việt hơn per-describe override trong cùng file, xét về mặt isolation và reporting?

Đáp án

Với testMatch per-project: mỗi project chạy hoàn toàn độc lập với worker riêng — không có nguy cơ context leaking giữa các role. HTML report tách rõ theo project name (admin / user / guest), dễ xác định role nào fail. Mỗi test file chỉ thuộc một role — không cần khai báo test.use() lặp lại. Ngược lại, per-describe trong cùng file tuy linh hoạt cho cross-role test nhưng phức tạp hơn khi debug, report không tách role rõ ràng, và dễ nhầm lẫn nếu describe nesting phức tạp.

Câu 5

Một test dùng storageState với JWT token có expiry 2 giờ. CI job chạy từ 8:00 AM — setup project login lúc 8:05 AM sinh ra user.json. Test suite gồm 300 test bắt đầu chạy song song từ 8:06 AM. Test cuối chạy lúc 10:10 AM. Điều gì sẽ xảy ra với các test chạy sau 10:05 AM?

Đáp án

JWT token trong user.json được sinh lúc 8:05 AM với expiry 2 giờ — token expire lúc 10:05 AM. Các test chạy sau 10:05 AM sẽ dùng context với token đã hết hạn. App server sẽ từ chối request (401 Unauthorized) hoặc redirect về login. Các test này sẽ fail — thường báo lỗi "Expected URL '/dashboard' but got '/login'" hoặc API response 401. Fix: rút ngắn thời gian chạy test suite (tăng parallelism), hoặc config setup project sinh state dùng refresh token, hoặc dùng API login với long-lived session thay vì JWT ngắn hạn.

15

Bài Tiếp Theo

Bài 13 tiếp tục nhóm Options Fixtures với video, screenshot, và trace — ba option kiểm soát artifact được ghi lại trong quá trình test, quan trọng cho debugging và CI reporting.

Bài 13: Option Fixture video, screenshot, trace