test-ui-qsf
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseQSF 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
skill — this skill only adds what is QSF-specific.
test-uiQSF Playwright测试套件的项目专属约定。有关通用Playwright最佳实践(定位器、自动等待、POM、反模式),请遵循技能——本技能仅补充QSF专属内容。
test-uiCommands
命令
- — run all tests headless
source ./bin/load_env.sh && bunx playwright test - — skip auth-setup
source ./bin/load_env.sh && bunx playwright test --project=regression-tests - /
bun run test— interactive UI modebunx playwright test --ui - — verbose terminal output
bunx playwright test --reporter=list - — run tagged tests
bunx playwright test --grep '@smoke' - — inspect failure traces
bunx playwright show-trace <trace.zip>
- —— 以无头模式运行所有测试
source ./bin/load_env.sh && bunx playwright test - —— 跳过认证设置
source ./bin/load_env.sh && bunx playwright test --project=regression-tests - /
bun run test—— 交互式UI模式bunx playwright test --ui - —— 详细终端输出
bunx playwright test --reporter=list - —— 运行标记测试
bunx playwright test --grep '@smoke' - —— 检查失败追踪信息
bunx playwright show-trace <trace.zip>
Test IDs
测试ID
getByTestIddata-test-labeltestIdAttributeplaywright.config.tstypescript
page.getByTestId("no-handles-available-indicator");
page.getByTestId("open-filter-section-button");getByTestIddata-test-labelplaywright.config.tstestIdAttributetypescript
page.getByTestId("no-handles-available-indicator");
page.getByTestId("open-filter-section-button");Fixtures (Auth Pattern)
测试夹具(认证模式)
The project extends Playwright's with pre-authenticated user fixtures via
storage state files in .
testplaywright-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的以支持预认证用户夹具。
playwright-tests/.auth/testtypescript
// 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 (), which composes
, , and . Specialized pages (e.g.
) add components like and . Keep
under 300 lines — split by element type when it grows.
BasePagepageObjects/BasePage.tsNavigationFilterSectionIdleModalAppsDetailPageFileUploadIwaFormElements.ts页面继承自(),该类集成了、和。专用页面(如)会添加和等组件。保持代码量低于300行——当代码增长时按元素类型拆分。
BasePagepageObjects/BasePage.tsNavigationFilterSectionIdleModalAppsDetailPageFileUploadIwaFormElements.tsEnums & Constants
枚举与常量
Store UI labels, error messages, and handle identifiers in
. Never hard-code them in tests.
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', ... };将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 navigation + upload + dashboard assertion is duplicated across
describe blocks, extract a shared helper (e.g.
).
beforeEachsetupDashboardForTestcase(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);
});
});当中的导航、上传和仪表板断言在多个describe块中重复时,提取为共享辅助函数(如)。
beforeEachsetupDashboardForTestcase(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 ; the variable controls
the target URL (default: ).
source ./bin/load_env.shENVIRONMENTtesttypescript
import { isOpenAmEnv } from "../../src/utils/environment";
test.skip(isOpenAmEnv, "此测试仅适用于Keycloak环境");环境通过加载;变量控制目标URL(默认值:)。
source ./bin/load_env.shENVIRONMENTtestConfiguration Highlights (playwright.config.ts
)
playwright.config.ts配置要点(playwright.config.ts
)
playwright.config.tstypescript
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检查清单
- /
testimported fromexpect, notauthFixtures@playwright/test - Named user fixtures (,
guest,testView) used for authtestNoRights - Browser context closed after each fixture via
context.close() - Handle names, errors, and labels imported from
enums/ - values match
getByTestIdattributesdata-test-label - used for environment-specific tests
test.skip(isOpenAmEnv, ...) - Env loaded via before running
source ./bin/load_env.sh - Generic rules from the skill also satisfied
test-ui
- /
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