prowler-test-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGeneric Patterns: For base Playwright patterns (Page Object Model, selectors, helpers), see theskill. This skill covers Prowler-specific conventions only.playwright
通用模式:关于Playwright的基础模式(Page Object Model、选择器、工具类),请查看技能文档。 本技能仅涵盖Prowler专属的约定规范。playwright
Prowler UI Test Structure
Prowler UI测试结构
ui/tests/
├── base-page.ts # Prowler-specific base page
├── helpers.ts # Prowler test utilities
└── {page-name}/
├── {page-name}-page.ts # Page Object Model
├── {page-name}.spec.ts # ALL tests (single file per feature)
└── {page-name}.md # Test documentation (MANDATORY - sync with spec.ts)ui/tests/
├── base-page.ts # Prowler专属基础页面
├── helpers.ts # Prowler测试工具类
└── {page-name}/
├── {page-name}-page.ts # 页面对象模型(Page Object Model)
├── {page-name}.spec.ts # 所有测试用例(每个功能对应单个文件)
└── {page-name}.md # 测试文档(必填 - 需与spec.ts同步)MANDATORY Checklist (Create or Modify Tests)
必检清单(创建或修改测试)
⚠️ ALWAYS verify BEFORE completing any E2E task:
⚠️ 完成任何E2E任务前必须验证以下内容:
When CREATING new tests:
创建新测试时:
- - Page Object created/updated
{page-name}-page.ts - - Tests added with correct tags (@TEST-ID)
{page-name}.spec.ts - - Documentation created with ALL test cases
{page-name}.md - Test IDs in match tags in
.md.spec.ts
- - 已创建/更新页面对象
{page-name}-page.ts - - 已添加测试并使用正确标签(@TEST-ID)
{page-name}.spec.ts - - 已创建包含所有测试用例的文档
{page-name}.md - 中的测试ID与
.md中的标签匹配.spec.ts
When MODIFYING existing tests:
修改现有测试时:
- MUST be updated if:
{page-name}.md- Test cases were added/removed
- Test flow changed (steps)
- Preconditions or expected results changed
- Tags or priorities changed
- Test IDs synchronized between and
.md.spec.ts
- 若出现以下情况,必须更新:
{page-name}.md- 添加/移除了测试用例
- 测试流程(步骤)发生变化
- 前置条件或预期结果有变更
- 标签或优先级被修改
- 和
.md中的测试ID保持同步.spec.ts
Quick validation:
快速验证命令:
bash
undefinedbash
undefinedVerify .md exists for each test folder
验证每个测试文件夹下是否存在.md文件
ls ui/tests/{feature}/{feature}.md
ls ui/tests/{feature}/{feature}.md
Verify test IDs match
验证测试ID是否匹配
grep -o "@[A-Z]-E2E-[0-9]" ui/tests/{feature}/{feature}.spec.ts | sort -u
grep -o "`[A-Z]-E2E-[0-9]`" ui/tests/{feature}/{feature}.md | sort -u
**❌ An E2E change is NOT considered complete without updating the corresponding .md file**
---grep -o "@[A-Z]-E2E-[0-9]" ui/tests/{feature}/{feature}.spec.ts | sort -u
grep -o "`[A-Z]-E2E-[0-9]`" ui/tests/{feature}/{feature}.md | sort -u
**❌ 若未更新对应的.md文件,E2E代码变更不视为完成**
---MCP Workflow - CRITICAL
MCP工作流 - 关键要求
⚠️ MANDATORY: If Playwright MCP tools are available, ALWAYS use them BEFORE creating tests.
- Navigate to target page
- Take snapshot to see actual DOM structure
- Interact with forms/elements to verify real flow
- Document actual selectors from snapshots
- Only then write test code
Why: Prevents tests based on assumptions. Real exploration = stable tests.
⚠️ 强制要求:若Playwright MCP工具可用,编写测试前必须先使用该工具。
- 导航至目标页面
- 拍摄快照查看实际DOM结构
- 交互表单/元素以验证真实流程
- 记录快照中的实际选择器
- 之后再编写测试代码
原因:避免基于假设编写测试。实际探索能产出更稳定的测试用例。
Wait Strategies (CRITICAL)
等待策略(关键要求)
⚠️ NEVER use - it causes flaky tests!
networkidle| Strategy | Use Case |
|---|---|
❌ | NEVER - flaky with polling/WebSockets |
⚠️ | Only when absolutely necessary |
✅ | PREFERRED - wait for specific UI state |
✅ | Wait for navigation |
✅ | BEST - encapsulated verification |
GOOD:
typescript
await homePage.verifyPageLoaded();
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible();BAD:
typescript
await page.waitForLoadState("networkidle"); // ❌ FLAKY
await page.waitForTimeout(2000); // ❌ ARBITRARY WAIT⚠️ 绝不要使用 - 会导致测试不稳定!
networkidle| 策略 | 使用场景 |
|---|---|
❌ | 绝对不要使用 - 在轮询/WebSocket场景下不稳定 |
⚠️ | 仅在绝对必要时使用 |
✅ | 首选 - 等待特定UI状态 |
✅ | 等待页面导航完成 |
✅ | 最佳方案 - 封装式验证 |
正确示例:
typescript
await homePage.verifyPageLoaded();
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible();错误示例:
typescript
await page.waitForLoadState("networkidle"); // ❌ 不稳定
await page.waitForTimeout(2000); // ❌ 任意等待时间Prowler Base Page
Prowler基础页面
typescript
import { Page, Locator, expect } from "@playwright/test";
export class BasePage {
constructor(protected page: Page) {}
async goto(path: string): Promise<void> {
await this.page.goto(path);
// Child classes should override verifyPageLoaded() to wait for specific elements
}
// Override in child classes to wait for page-specific elements
async verifyPageLoaded(): Promise<void> {
await expect(this.page.locator("main")).toBeVisible();
}
// Prowler-specific: notification handling
async waitForNotification(): Promise<Locator> {
const notification = this.page.locator('[role="status"]');
await notification.waitFor({ state: "visible" });
return notification;
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = await this.waitForNotification();
await expect(notification).toContainText(message);
}
}typescript
import { Page, Locator, expect } from "@playwright/test";
export class BasePage {
constructor(protected page: Page) {}
async goto(path: string): Promise<void> {
await this.page.goto(path);
// 子类应重写verifyPageLoaded()以等待特定元素
}
// 在子类中重写,等待页面专属元素
async verifyPageLoaded(): Promise<void> {
await expect(this.page.locator("main")).toBeVisible();
}
// Prowler专属:通知处理
async waitForNotification(): Promise<Locator> {
const notification = this.page.locator('[role="status"]');
await notification.waitFor({ state: "visible" });
return notification;
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = await this.waitForNotification();
await expect(notification).toContainText(message);
}
}Page Navigation Verification Pattern
页面导航验证模式
⚠️ URL assertions belong in Page Objects, NOT in tests!
When verifying redirects or page navigation, create dedicated methods in the target Page Object:
typescript
// ✅ GOOD - In SignInPage
async verifyOnSignInPage(): Promise<void> {
await expect(this.page).toHaveURL(/\/sign-in/);
await expect(this.pageTitle).toBeVisible();
}
// ✅ GOOD - In test
await homePage.goto(); // Try to access protected route
await signInPage.verifyOnSignInPage(); // Verify redirect
// ❌ BAD - Direct assertions in test
await homePage.goto();
await expect(page).toHaveURL(/\/sign-in/); // Should be in Page Object
await expect(page.getByText("Sign in")).toBeVisible();Naming convention: for redirect verification methods.
verifyOn{PageName}Page()⚠️ URL断言应放在页面对象中,而非测试代码里!
验证重定向或页面导航时,在目标页面对象中创建专用方法:
typescript
// ✅ 正确示例 - 在SignInPage中
async verifyOnSignInPage(): Promise<void> {
await expect(this.page).toHaveURL(/\/sign-in/);
await expect(this.pageTitle).toBeVisible();
}
// ✅ 正确示例 - 在测试代码中
await homePage.goto(); // 尝试访问受保护路由
await signInPage.verifyOnSignInPage(); // 验证重定向
// ❌ 错误示例 - 直接在测试中断言
await homePage.goto();
await expect(page).toHaveURL(/\/sign-in/); // 应放在页面对象中
await expect(page.getByText("Sign in")).toBeVisible();命名约定: 重定向验证方法命名为。
verifyOn{PageName}Page()Prowler-Specific Pages
Prowler专属页面示例
Providers Page
云服务商页面(Providers Page)
typescript
import { BasePage } from "../base-page";
export class ProvidersPage extends BasePage {
readonly addButton = this.page.getByRole("button", { name: "Add Provider" });
readonly providerTable = this.page.getByRole("table");
async goto(): Promise<void> {
await super.goto("/providers");
}
async addProvider(type: string, alias: string): Promise<void> {
await this.addButton.click();
await this.page.getByLabel("Provider Type").selectOption(type);
await this.page.getByLabel("Alias").fill(alias);
await this.page.getByRole("button", { name: "Create" }).click();
}
}typescript
import { BasePage } from "../base-page";
export class ProvidersPage extends BasePage {
readonly addButton = this.page.getByRole("button", { name: "Add Provider" });
readonly providerTable = this.page.getByRole("table");
async goto(): Promise<void> {
await super.goto("/providers");
}
async addProvider(type: string, alias: string): Promise<void> {
await this.addButton.click();
await this.page.getByLabel("Provider Type").selectOption(type);
await this.page.getByLabel("Alias").fill(alias);
await this.page.getByRole("button", { name: "Create" }).click();
}
}Scans Page
扫描页面(Scans Page)
typescript
export class ScansPage extends BasePage {
readonly newScanButton = this.page.getByRole("button", { name: "New Scan" });
readonly scanTable = this.page.getByRole("table");
async goto(): Promise<void> {
await super.goto("/scans");
}
async startScan(providerAlias: string): Promise<void> {
await this.newScanButton.click();
await this.page.getByRole("combobox", { name: "Provider" }).click();
await this.page.getByRole("option", { name: providerAlias }).click();
await this.page.getByRole("button", { name: "Start Scan" }).click();
}
}typescript
export class ScansPage extends BasePage {
readonly newScanButton = this.page.getByRole("button", { name: "New Scan" });
readonly scanTable = this.page.getByRole("table");
async goto(): Promise<void> {
await super.goto("/scans");
}
async startScan(providerAlias: string): Promise<void> {
await this.newScanButton.click();
await this.page.getByRole("combobox", { name: "Provider" }).click();
await this.page.getByRole("option", { name: providerAlias }).click();
await this.page.getByRole("button", { name: "Start Scan" }).click();
}
}Test Tags for Prowler
Prowler测试标签规范
typescript
test("Provider CRUD operations",
{ tag: ["@critical", "@e2e", "@providers", "@PROV-E2E-001"] },
async ({ page }) => {
// ...
}
);| Category | Tags |
|---|---|
| Priority | |
| Type | |
| Feature | |
| Test ID | |
typescript
test("Provider CRUD operations",
{ tag: ["@critical", "@e2e", "@providers", "@PROV-E2E-001"] },
async ({ page }) => {
// ...
}
);| 分类 | 标签 |
|---|---|
| 优先级 | |
| 类型 | |
| 功能 | |
| 测试ID | |
Prowler Test Documentation Template
Prowler测试文档模板
Keep under 60 lines. Focus on flow, preconditions, expected results only.
markdown
undefined文档长度控制在60行以内。仅聚焦流程、前置条件和预期结果。
markdown
undefinedE2E Tests: {Feature Name}
E2E测试:{功能名称}
Suite ID:
Feature: {Feature description}
{SUITE-ID}套件ID:
功能: {功能描述}
{SUITE-ID}Test Case: {TEST-ID}
- {Test case title}
{TEST-ID}测试用例: {TEST-ID}
- {测试用例标题}
{TEST-ID}Priority:
Tags: @e2e, @{feature-name}
{critical|high|medium|low}Preconditions:
- {Prerequisites}
优先级:
标签: @e2e, @{feature-name}
{critical|high|medium|low}前置条件:
- {前置要求}
Flow Steps:
流程步骤:
- {Step}
- {Step}
- {步骤}
- {步骤}
Expected Result:
预期结果:
- {Outcome}
- {结果}
Key Verification Points:
关键验证点:
- {Assertion}
---- {断言内容}
---Commands
命令行指令
bash
cd ui && pnpm run test:e2e # All tests
cd ui && pnpm run test:e2e tests/providers/ # Specific folder
cd ui && pnpm run test:e2e --grep "provider" # By pattern
cd ui && pnpm run test:e2e:ui # With UI
cd ui && pnpm run test:e2e:debug # Debug mode
cd ui && pnpm run test:e2e:headed # See browser
cd ui && pnpm run test:e2e:report # Generate reportbash
cd ui && pnpm run test:e2e # 运行所有测试
cd ui && pnpm run test:e2e tests/providers/ # 运行指定文件夹下的测试
cd ui && pnpm run test:e2e --grep "provider" # 按匹配模式运行测试
cd ui && pnpm run test:e2e:ui # 带UI界面运行测试
cd ui && pnpm run test:e2e:debug # 调试模式运行测试
cd ui && pnpm run test:e2e:headed # 显示浏览器运行测试
cd ui && pnpm run test:e2e:report # 生成测试报告Resources
参考资源
- Documentation: See references/ for links to local developer guide
- 文档: 查看[references/]获取本地开发者指南链接