关于 prowler-test-ui
prowler-test-ui 是面向 Prowler UI 端到端测试开发的技能,提供项目专用的 Playwright 约定。它强调测试、页面对象和 Markdown 文档保持同步,并在编写测试前通过 MCP 工具探索实际页面。
主要功能
- 01规定 UI 测试目录结构
- 02要求测试与文档同步
- 03编写测试前探索实际页面
- 04提供等待与导航验证规范
- 05定义测试标签及运行命令
适用场景
- 01为 Prowler 页面新增端到端测试
- 02修改测试时同步用例文档
- 03根据实际页面结构编写稳定测试
MCP 相关
说明了 Playwright MCP 工具的使用流程:编写测试前先导航、检查页面快照并与页面交互,以确认真实结构和选择器。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ prowler-cloud/ prowler --skill prowler-test-ui安装后在 Claude Code 中输入/prowler-test-ui确认已加载。
附带文件references/e2e-docs.md
详细介绍
通用模式:有关基础 Playwright 模式(页面对象模型、选择器、辅助函数),请参阅
playwright技能。 此技能仅涵盖 Prowler 特定约定。
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)
强制检查清单(创建或修改测试)
⚠️ 完成任何 E2E 任务前,务必进行验证:
创建新测试时
-
{page-name}-page.ts- 已创建/更新页面对象 -
{page-name}.spec.ts- 已添加带正确标签(@TEST-ID)的测试 -
{page-name}.md- 已创建包含所有测试用例的文档 -
.md中的测试 ID 与.spec.ts中的标签一致
修改现有测试时
- 若出现以下情况,必须更新
{page-name}.md:- 添加/删除了测试用例
- 测试流程(步骤)发生变化
- 前置条件或预期结果发生变化
- 标签或优先级发生变化
-
.md与.spec.ts中的测试 ID 保持同步
快速验证
# Verify .md exists for each test folder
ls ui/tests/{feature}/{feature}.md
# Verify test IDs match
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 工作流 - 至关重要
⚠️ 强制要求:如果可以使用 Playwright MCP 工具,创建测试前务必先使用这些工具。
- 导航到目标页面
- 获取快照以查看实际 DOM 结构
- 与表单/元素交互以验证真实流程
- 记录快照中的实际选择器
- 然后再编写测试代码
原因:避免测试建立在假设之上。实际探索 = 稳定的测试。
等待策略(至关重要)
⚠️ 切勿使用 networkidle - 这会导致测试不稳定!
| 策略 | 使用场景 |
|---|---|
❌ networkidle | 切勿使用 - 在轮询/WebSockets 场景下会导致测试不稳定 |
⚠️ load | 仅在绝对必要时使用 |
✅ expect(element).toBeVisible() | 首选 - 等待特定 UI 状态 |
✅ page.waitForURL() | 等待导航 |
✅ pageObject.verifyPageLoaded() | 最佳 - 封装验证逻辑 |
正确示例:
await homePage.verifyPageLoaded();
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible();
错误示例:
await page.waitForLoadState("networkidle"); // ❌ FLAKY
await page.waitForTimeout(2000); // ❌ ARBITRARY WAIT
Prowler 基础页面
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);
}
}
页面导航验证模式
⚠️ URL 断言应放在页面对象中,而不是测试中!
验证重定向或页面导航时,应在目标页面对象中创建专用方法:
// ✅ 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();
**命名约定:**重定向验证方法使用 verifyOn{PageName}Page()。
Prowler 特定页面
Providers 页面
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 页面
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();
}
}
Prowler 测试标签
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 测试文档模板
控制在 60 行以内。仅关注流程、前置条件和预期结果。
### E2E Tests: {Feature Name}
**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}
---
## Test Case: `{TEST-ID}` - {Test case title}
**Priority:** `{critical|high|medium|low}`
**Tags:** @e2e, @{feature-name}
**Preconditions:**
- {Prerequisites}
### Flow Steps:
1. {Step}
2. {Step}
### Expected Result:
- {Outcome}
### Key Verification Points:
- {Assertion}
命令
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
资源
- 文档:有关本地开发者指南的链接,请参阅 references/
评论
支持 Markdown