prowler-test-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Generic Patterns: For base Playwright patterns (Page Object Model, selectors, helpers), see the
playwright
skill. This skill covers Prowler-specific conventions only.
通用模式:关于Playwright的基础模式(Page Object Model、选择器、工具类),请查看
playwright
技能文档。 本技能仅涵盖Prowler专属的约定规范。

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-name}-page.ts
    - Page Object created/updated
  • {page-name}.spec.ts
    - Tests added with correct tags (@TEST-ID)
  • {page-name}.md
    - Documentation created with ALL test cases
  • Test IDs in
    .md
    match tags in
    .spec.ts
  • {page-name}-page.ts
    - 已创建/更新页面对象
  • {page-name}.spec.ts
    - 已添加测试并使用正确标签(@TEST-ID)
  • {page-name}.md
    - 已创建包含所有测试用例的文档
  • .md
    中的测试ID与
    .spec.ts
    中的标签匹配

When MODIFYING existing tests:

修改现有测试时:

  • {page-name}.md
    MUST be updated if:
    • Test cases were added/removed
    • Test flow changed (steps)
    • Preconditions or expected results changed
    • Tags or priorities changed
  • Test IDs synchronized between
    .md
    and
    .spec.ts
  • 若出现以下情况,必须更新
    {page-name}.md
    • 添加/移除了测试用例
    • 测试流程(步骤)发生变化
    • 前置条件或预期结果有变更
    • 标签或优先级被修改
  • .md
    .spec.ts
    中的测试ID保持同步

Quick validation:

快速验证命令:

bash
undefined
bash
undefined

Verify .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.
  1. Navigate to target page
  2. Take snapshot to see actual DOM structure
  3. Interact with forms/elements to verify real flow
  4. Document actual selectors from snapshots
  5. Only then write test code
Why: Prevents tests based on assumptions. Real exploration = stable tests.

⚠️ 强制要求:若Playwright MCP工具可用,编写测试前必须先使用该工具。
  1. 导航至目标页面
  2. 拍摄快照查看实际DOM结构
  3. 交互表单/元素以验证真实流程
  4. 记录快照中的实际选择器
  5. 之后再编写测试代码
原因:避免基于假设编写测试。实际探索能产出更稳定的测试用例。

Wait Strategies (CRITICAL)

等待策略(关键要求)

⚠️ NEVER use
networkidle
- it causes flaky tests!
StrategyUse Case
networkidle
NEVER - flaky with polling/WebSockets
⚠️
load
Only when absolutely necessary
expect(element).toBeVisible()
PREFERRED - wait for specific UI state
page.waitForURL()
Wait for navigation
pageObject.verifyPageLoaded()
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
- 会导致测试不稳定!
策略使用场景
networkidle
绝对不要使用 - 在轮询/WebSocket场景下不稳定
⚠️
load
仅在绝对必要时使用
expect(element).toBeVisible()
首选 - 等待特定UI状态
page.waitForURL()
等待页面导航完成
pageObject.verifyPageLoaded()
最佳方案 - 封装式验证
正确示例:
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:
verifyOn{PageName}Page()
for redirect verification methods.

⚠️ 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 }) => {
    // ...
  }
);
CategoryTags
Priority
@critical
,
@high
,
@medium
,
@low
Type
@e2e
,
@smoke
,
@regression
Feature
@providers
,
@scans
,
@findings
,
@compliance
,
@signin
,
@signup
Test ID
@PROV-E2E-001
,
@SCAN-E2E-002

typescript
test("Provider CRUD operations",
  { tag: ["@critical", "@e2e", "@providers", "@PROV-E2E-001"] },
  async ({ page }) => {
    // ...
  }
);
分类标签
优先级
@critical
,
@high
,
@medium
,
@low
类型
@e2e
,
@smoke
,
@regression
功能
@providers
,
@scans
,
@findings
,
@compliance
,
@signin
,
@signup
测试ID
@PROV-E2E-001
,
@SCAN-E2E-002

Prowler Test Documentation Template

Prowler测试文档模板

Keep under 60 lines. Focus on flow, preconditions, expected results only.
markdown
undefined
文档长度控制在60行以内。仅聚焦流程、前置条件和预期结果。
markdown
undefined

E2E Tests: {Feature Name}

E2E测试:{功能名称}

Suite ID:
{SUITE-ID}
Feature: {Feature description}

套件ID:
{SUITE-ID}
功能: {功能描述}

Test Case:
{TEST-ID}
- {Test case title}

测试用例:
{TEST-ID}
- {测试用例标题}

Priority:
{critical|high|medium|low}
Tags: @e2e, @{feature-name}
Preconditions:
  • {Prerequisites}
优先级:
{critical|high|medium|low}
标签: @e2e, @{feature-name}
前置条件:
  • {前置要求}

Flow Steps:

流程步骤:

  1. {Step}
  2. {Step}
  1. {步骤}
  2. {步骤}

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 report
bash
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/]获取本地开发者指南链接