test-ui-qsf

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

QSF Playwright E2E Conventions

QSF Playwright E2E测试套件约定

Project-specific conventions for the QSF Playwright suite. For generic Playwright best practices (locators, auto-waiting, POM, anti-patterns), follow the
test-ui
skill — this skill only adds what is QSF-specific.
QSF Playwright测试套件的项目专属约定。有关通用Playwright最佳实践(定位器、自动等待、POM、反模式),请遵循
test-ui
技能——本技能仅补充QSF专属内容。

Commands

命令

  • source ./bin/load_env.sh && bunx playwright test
    — run all tests headless
  • source ./bin/load_env.sh && bunx playwright test --project=regression-tests
    — skip auth-setup
  • bun run test
    /
    bunx playwright test --ui
    — interactive UI mode
  • bunx playwright test --reporter=list
    — verbose terminal output
  • bunx playwright test --grep '@smoke'
    — run tagged tests
  • bunx playwright show-trace <trace.zip>
    — inspect failure traces
  • source ./bin/load_env.sh && bunx playwright test
    —— 以无头模式运行所有测试
  • source ./bin/load_env.sh && bunx playwright test --project=regression-tests
    —— 跳过认证设置
  • bun run test
    /
    bunx playwright test --ui
    —— 交互式UI模式
  • bunx playwright test --reporter=list
    —— 详细终端输出
  • bunx playwright test --grep '@smoke'
    —— 运行标记测试
  • bunx playwright show-trace <trace.zip>
    —— 检查失败追踪信息

Test IDs

测试ID

getByTestId
resolves to the
data-test-label
attribute (set via
testIdAttribute
in
playwright.config.ts
):
typescript
page.getByTestId("no-handles-available-indicator");
page.getByTestId("open-filter-section-button");
getByTestId
对应**
data-test-label
**属性(通过
playwright.config.ts
中的
testIdAttribute
配置):
typescript
page.getByTestId("no-handles-available-indicator");
page.getByTestId("open-filter-section-button");

Fixtures (Auth Pattern)

测试夹具(认证模式)

The project extends Playwright's
test
with pre-authenticated user fixtures via storage state files in
playwright-tests/.auth/
.
typescript
// fixtures/authFixtures.ts
const authFiles = {
  guest: "guest_storage_state.json",
  testView: "test_view_storage_state.json",
  testNoRights: "test_no_rights_storage_state.json",
  // ... more users
};

const baseFixtures = Object.fromEntries(
  Object.entries(authFiles).map(([key, fileName]) => [
    key,
    async ({ browser }: { browser: Browser }, use: (page: Page) => Promise<void>) => {
      const storageState = path.join(__dirname, "../.auth", fileName);
      const context = await browser.newContext({ storageState });
      const page = await context.newPage();
      try {
        await use(page);
      } finally {
        await context.close();
      }
    },
  ]),
);

export const test = baseTest.extend(baseFixtures);
export const expect = test.expect;
本项目通过
playwright-tests/.auth/
目录下的存储状态文件,扩展Playwright的
test
以支持预认证用户夹具。
typescript
// fixtures/authFixtures.ts
const authFiles = {
  guest: "guest_storage_state.json",
  testView: "test_view_storage_state.json",
  testNoRights: "test_no_rights_storage_state.json",
  // ... 更多用户
};

const baseFixtures = Object.fromEntries(
  Object.entries(authFiles).map(([key, fileName]) => [
    key,
    async ({ browser }: { browser: Browser }, use: (page: Page) => Promise<void>) => {
      const storageState = path.join(__dirname, "../.auth", fileName);
      const context = await browser.newContext({ storageState });
      const page = await context.newPage();
      try {
        await use(page);
      } finally {
        await context.close();
      }
    },
  ]),
);

export const test = baseTest.extend(baseFixtures);
export const expect = test.expect;

Fixture Usage

夹具使用

typescript
// Tests import from authFixtures, NOT from @playwright/test
import { test, expect } from "../../fixtures/authFixtures";

test("user sees their handles", async ({ guest }) => {
  await guest.goto("/"); // `guest` is a pre-authenticated Page — no login needed
});

test("restricted user cannot access admin", async ({ testNoRights }) => {
  await testNoRights.goto("/admin");
});
typescript
// 测试从authFixtures导入,而非@playwright/test
import { test, expect } from "../../fixtures/authFixtures";

test("用户可查看其句柄", async ({ guest }) => {
  await guest.goto("/"); // `guest`是预认证的Page实例——无需登录
});

test("受限用户无法访问管理页面", async ({ testNoRights }) => {
  await testNoRights.goto("/admin");
});

File Organization

文件组织结构

text
playwright-tests/
├── .auth/              # Storage state JSON files (gitignored in CI)
├── enums/              # Constants: handle names, error messages, labels
├── fixtures/           # authFixtures.ts — extended test/expect
├── pageObjects/        # POM classes (BasePage, LoginPage, ...)
│   └── components/     # Reusable: Navigation, FileUpload, FormElements
├── setup/              # auth.setup.ts, cleanup.ts
└── tests/              # Spec files grouped by feature (handle/, ping-files/, ...)
text
playwright-tests/
├── .auth/              # 存储状态JSON文件(CI中已忽略Git提交)
├── enums/              # 常量:句柄名称、错误信息、标签
├── fixtures/           # authFixtures.ts —— 扩展后的test/expect
├── pageObjects/        # POM类(BasePage、LoginPage等)
│   └── components/     # 可复用组件:Navigation、FileUpload、FormElements
├── setup/              # auth.setup.ts、cleanup.ts
└── tests/              # 按功能分组的测试文件(handle/、ping-files/等)

Page Objects

页面对象

Pages extend
BasePage
(
pageObjects/BasePage.ts
), which composes
Navigation
,
FilterSection
, and
IdleModal
. Specialized pages (e.g.
AppsDetailPage
) add components like
FileUpload
and
Iwa
. Keep
FormElements.ts
under 300 lines — split by element type when it grows.
页面继承自
BasePage
pageObjects/BasePage.ts
),该类集成了
Navigation
FilterSection
IdleModal
。专用页面(如
AppsDetailPage
)会添加
FileUpload
Iwa
等组件。保持
FormElements.ts
代码量低于300行——当代码增长时按元素类型拆分。

Enums & Constants

枚举与常量

Store UI labels, error messages, and handle identifiers in
playwright-tests/enums/
. Never hard-code them in tests.
typescript
// enums/handles.ts
export const HandleNames = { PING_FILES: '___ping-files-en', ... };
export const HandleIds = { PING_FILES: 'qsf-handle-ping-files', ... };

// enums/errors.ts
export const Errors = { FILE_TOO_LARGE: 'produced an error while processing', ... };

// enums/elementLabels.ts
export const Buttons = { SUBMIT: 'Submit', UPLOAD: 'Upload files', ... };
将UI标签、错误信息和句柄标识符存储在
playwright-tests/enums/
目录下。绝对不要在测试中硬编码这些内容。
typescript
// enums/handles.ts
export const HandleNames = { PING_FILES: '___ping-files-en', ... };
export const HandleIds = { PING_FILES: 'qsf-handle-ping-files', ... };

// enums/errors.ts
export const Errors = { FILE_TOO_LARGE: 'produced an error while processing', ... };

// enums/elementLabels.ts
export const Buttons = { SUBMIT: 'Submit', UPLOAD: 'Upload files', ... };

Typical Test Pattern

典型测试模式

typescript
import { test, expect } from "../../fixtures/authFixtures";
import { AppsDetailPage } from "../../pageObjects/AppsDetailPage";
import { MyAppsPage } from "../../pageObjects/MyAppsPage";
import { HandleNames } from "../../enums/handles";

test.describe("Feature Area", () => {
  test.beforeEach(async ({ guest }) => {
    const myAppsPage = new MyAppsPage(guest);
    await guest.goto("/");
    await myAppsPage.navigation.openHandle(HandleNames.PING_FILES);
  });

  test("describes expected behavior", async ({ guest }) => {
    const appsDetailPage = new AppsDetailPage(guest);
    await appsDetailPage.doSomething();
    await appsDetailPage.assertSomething(expected);
  });
});
When
beforeEach
navigation + upload + dashboard assertion is duplicated across describe blocks, extract a shared helper (e.g.
setupDashboardForTestcase(guest, handleName, testcase)
).
typescript
import { test, expect } from "../../fixtures/authFixtures";
import { AppsDetailPage } from "../../pageObjects/AppsDetailPage";
import { MyAppsPage } from "../../pageObjects/MyAppsPage";
import { HandleNames } from "../../enums/handles";

test.describe("功能区域", () => {
  test.beforeEach(async ({ guest }) => {
    const myAppsPage = new MyAppsPage(guest);
    await guest.goto("/");
    await myAppsPage.navigation.openHandle(HandleNames.PING_FILES);
  });

  test("描述预期行为", async ({ guest }) => {
    const appsDetailPage = new AppsDetailPage(guest);
    await appsDetailPage.doSomething();
    await appsDetailPage.assertSomething(expected);
  });
});
beforeEach
中的导航、上传和仪表板断言在多个describe块中重复时,提取为共享辅助函数(如
setupDashboardForTestcase(guest, handleName, testcase)
)。

Environment-Aware Tests

环境感知测试

typescript
import { isOpenAmEnv } from "../../src/utils/environment";

test.skip(isOpenAmEnv, "This test is only for Keycloak environments");
Env is loaded via
source ./bin/load_env.sh
; the
ENVIRONMENT
variable controls the target URL (default:
test
).
typescript
import { isOpenAmEnv } from "../../src/utils/environment";

test.skip(isOpenAmEnv, "此测试仅适用于Keycloak环境");
环境通过
source ./bin/load_env.sh
加载;
ENVIRONMENT
变量控制目标URL(默认值:
test
)。

Configuration Highlights (
playwright.config.ts
)

配置要点(
playwright.config.ts

typescript
export default defineConfig({
  testDir: "./playwright-tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.TEST_HANDLE ? 0 : 1,
  workers: process.env.CI ? 1 : 5,
  globalTimeout: 30 * 60 * 1000,
  timeout: 60000,
  expect: { timeout: 10000 },
  use: {
    ignoreHTTPSErrors: true,
    testIdAttribute: "data-test-label",
    baseURL: envUrl,
    trace: "on-first-retry",
  },
  projects: [
    { name: "auth-setup", testMatch: "**/setup/auth.setup.ts" },
    {
      name: "regression-tests",
      testMatch: ["**/tests/*.spec.ts", "**/tests/**/*.spec.ts"],
      dependencies: process.env.CI ? ["auth-setup"] : [],
      use: { trace: "retain-on-failure", headless: true },
    },
  ],
});
typescript
export default defineConfig({
  testDir: "./playwright-tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.TEST_HANDLE ? 0 : 1,
  workers: process.env.CI ? 1 : 5,
  globalTimeout: 30 * 60 * 1000,
  timeout: 60000,
  expect: { timeout: 10000 },
  use: {
    ignoreHTTPSErrors: true,
    testIdAttribute: "data-test-label",
    baseURL: envUrl,
    trace: "on-first-retry",
  },
  projects: [
    { name: "auth-setup", testMatch: "**/setup/auth.setup.ts" },
    {
      name: "regression-tests",
      testMatch: ["**/tests/*.spec.ts", "**/tests/**/*.spec.ts"],
      dependencies: process.env.CI ? ["auth-setup"] : [],
      use: { trace: "retain-on-failure", headless: true },
    },
  ],
});

QSF Checklist

QSF检查清单

  • test
    /
    expect
    imported from
    authFixtures
    , not
    @playwright/test
  • Named user fixtures (
    guest
    ,
    testView
    ,
    testNoRights
    ) used for auth
  • Browser context closed after each fixture via
    context.close()
  • Handle names, errors, and labels imported from
    enums/
  • getByTestId
    values match
    data-test-label
    attributes
  • test.skip(isOpenAmEnv, ...)
    used for environment-specific tests
  • Env loaded via
    source ./bin/load_env.sh
    before running
  • Generic rules from the
    test-ui
    skill also satisfied
  • test
    /
    expect
    authFixtures
    导入,而非
    @playwright/test
  • 使用命名用户夹具(
    guest
    testView
    testNoRights
    )进行认证
  • 每个夹具执行完毕后通过
    context.close()
    关闭浏览器上下文
  • 句柄名称、错误信息和标签从
    enums/
    导入
  • getByTestId
    值与
    data-test-label
    属性匹配
  • 环境专属测试使用
    test.skip(isOpenAmEnv, ...)
  • 运行前通过
    source ./bin/load_env.sh
    加载环境
  • 同时满足
    test-ui
    技能中的通用规则