算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
quality-e2e-testing【设计与创意】Use this skill when setting up E2E testing, end-to-end tests, Playwright, Cypress, browser tests, user flow tests, or integration tests. This skill enforces: framework selection (Playwright preferred), page object model, test isolation, parallel execution, CI integration, and visual assertions. Do NOT use for: unit testing, API-only tests, or perform…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Configure and write reliable end-to-end browser tests with framework selection, page objects, parallel execution, and CI integration.
Exact user phrases: "E2E test", "end-to-end", "Playwright", "Cypress", "Selenium", "browser test", "integration test", "user flow test", "test automation", "page object", "test isolation".
Before activating, verify:
E2E testing strategy with framework selection, page object patterns, and CI pipeline configuration.
# Framework selection with rationale
# Test structure and page object hierarchy
// Page object example
// CI pipeline configuration
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
200 lines of configuration and test patterns.
Playwright: cross-browser (Chromium, Firefox, WebKit), native async, network mocking, visual diffing, codegen, auto-wait. Cypress: easier debugging, time-travel, rich interactive runner, but Chromium-only + flakier cross-browser. Selenium: last resort — only if legacy compatibility required. Recommendation: Playwright for new projects.
npm init playwright@latest
Directory structure: e2e/ with pages/ (page objects), fixtures/ (test data), specs/ (test files), utils/ (helpers, custom assertions). Global setup file for auth state (login once, reuse across tests).
export class LoginPage {
constructor(private page: Page) {}
async goto() { await this.page.goto("/login"); }
async fillEmail(email: string) { await this.page.fill('[data-testid="email"]', email); }
async fillPassword(password: string) { await this.page.fill('[data-testid="password"]', password); }
async submit() { await this.page.click('[data-testid="submit"]'); }
async login(email: string, password: string) {
await this.fillEmail(email);
await this.fillPassword(password);
await this.submit();
}
}
Each test starts with a clean state. Reset database before test run (not per-test if slow). Use test.beforeEach for auth + navigation. API-mock external services. Never share page or context between tests. Use test.describe.serial only when sequential execution is explicitly required (rare).
Playwright: worker pool (default: CPU cores). Sharding in CI: npx playwright test --shard=1/4. Each shard runs a subset of tests independently. Test retries: 1–2 retries in CI for flaky tests — but target zero flakiness. Mark flaky tests with test.fixme() and track.
# GitHub Actions example
- name: Run E2E tests
run: npx playwright test
env:
CI: true
BASE_URL: ${{ secrets.E2E_BASE_URL }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
await expect(page).toHaveScreenshot("homepage.png", {
maxDiffPixels: 100,
threshold: 0.2,
});
Store baselines in version control. Update baselines intentionally: npx playwright test --update-snapshots.
// e2e/specs/checkout.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";
test.describe("Checkout Flow", () => {
let loginPage: LoginPage;
let cartPage: CartPage;
let checkoutPage: CheckoutPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
cartPage = new CartPage(page);
checkoutPage = new CheckoutPage(page);
});
test("should complete purchase with valid payment", async ({ page }) => {
await loginPage.goto();
await loginPage.loginAs("customer@example.com", "password123");
await cartPage.addItemToCart("Wireless Keyboard");
await cartPage.proceedToCheckout();
await checkoutPage.fillShippingInfo({
name: "John Doe",
address: "123 Main St",
city: "Portland",
zip: "97201",
});
await checkoutPage.selectPaymentMethod("credit_card");
await checkoutPage.submitOrder();
await expect(page.getByTestId("order-confirmation")).toBeVisible();
await expect(page.getByTestId("order-number")).not.toBeEmpty();
});
test("should show validation errors for incomplete form", async ({ page }) => {
await loginPage.goto();
await loginPage.loginAs("customer@example.com", "password123");
await cartPage.addItemToCart("Wireless Keyboard");
await cartPage.proceedToCheckout();
await checkoutPage.submitOrder();
await expect(page.getByTestId("field-error")).toHaveCount(4);
});
});
// e2e/pages/CheckoutPage.ts
import { Page, Locator } from "@playwright/test";
export class CheckoutPage {
readonly shippingForm: Locator;
readonly submitButton: Locator;
readonly orderConfirmation: Locator;
constructor(private page: Page) {
this.shippingForm = page.getByTestId("shipping-form");
this.submitButton = page.getByTestId("submit-order");
this.orderConfirmation = page.getByTestId("order-confirmation");
}
async fillShippingInfo(info: {
name: string;
address: string;
city: string;
zip: string;
}) {
await this.page.getByTestId("shipping-name").fill(info.name);
await this.page.getByTestId("shipping-address").fill(info.address);
await this.page.getByTestId("shipping-city").fill(info.city);
await this.page.getByTestId("shipping-zip").fill(info.zip);
}
async selectPaymentMethod(method: string) {
await this.page.getByTestId(`payment-${method}`).click();
}
async submitOrder() {
await this.submitButton.click();
}
async isOrderConfirmed(): Promise<boolean> {
return this.orderConfirmation.isVisible();
}
}
test("should handle payment failure gracefully", async ({ page }) => {
await page.route("**/api/payments", async (route) => {
await route.fulfill({
status: 402,
contentType: "application/json",
body: JSON.stringify({ error: "insufficient_funds" }),
});
});
await page.goto("/checkout");
await checkoutPage.submitOrder();
await expect(page.getByTestId("payment-error")).toHaveText(
"Insufficient funds. Please try another payment method."
);
});
// e2e/global-setup.ts
import { FullConfig } from "@playwright/test";
async function globalSetup(config: FullConfig) {
const { baseURL, storageState } = config.projects[0].use;
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`${baseURL}/login`);
await page.getByTestId("email").fill("e2e@example.com");
await page.getByTestId("password").fill(process.env.E2E_PASSWORD!);
await page.getByTestId("submit").click();
await page.waitForURL("**/dashboard");
await page.context().storageState({ path: storageState as string });
await browser.close();
}
name: E2E Tests
on:
pull_request:
branches: [main]
paths:
- "src/**"
- "e2e/**"
jobs:
e2e:
timeout-minutes: 30
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Start app
run: npm start & npx wait-on http://localhost:3000
- name: Run E2E tests
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: http://localhost:3000
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
CSS classes change frequently for styling and are not reliable test targets. Use data-testid attributes that are stable across styling changes. Never use text content for selectors (breaks with i18n). Never use XPath (brittle to DOM structure changes).
E2E tests that depend on state created by previous tests are fragile and hard to debug. Each test must set up its own state (via API seeding or UI actions). Use test.describe.serial only when absolutely necessary (and document why).
Using page.waitForTimeout(3000) creates flaky tests that either waste time or fail intermittently. Use auto-waiting locators (page.getByText('...'), page.waitForSelector, page.waitForResponse). Playwright automatically waits for elements to be visible, enabled, and stable.
Writing E2E tests for every single feature creates a slow, brittle, expensive test suite. Use E2E for critical user journeys only (happy path through main workflows). Test edge cases and error paths at lower levels (unit, integration).
E2E tests relying on shared test data or production data are non-deterministic and break when data changes. Use API seeding before each test. Clean up test data in afterEach. Use factories with unique generated values.
E2E tests that cover too many steps in a single test are hard to debug when they fail. A test should cover one user journey. If a journey has 10 steps and step 5 fails, you want to know exactly which step broke.
| Level | Characteristics | Practices |
|---|---|---|
| 1: Initial | Manual browser testing | No automation, no test scripts, relies entirely on manual QA |
| 2: Defined | Basic automation | Selenium/Cypress scripts for critical paths, unmaintained page objects, flaky tests accepted |
| 3: Managed | Structured E2E suite | Playwright/Cypress with page objects, data-testid selectors, parallel execution, CI integration |
| 4: Measured | Reliable E2E gates | Test isolation (API seeding), visual regression, flaky test quarantine, < 1% flake rate, 95%+ reliability |
| 5: Optimized | E2E as release confidence gate | Combined functional + visual + accessibility E2E, automatic baseline updates, predictive test selection, self-healing selectors |
// e2e/fixtures/seed.ts
import { APIRequestContext } from "@playwright/test";
export async function seedTestData(request: APIRequestContext) {
const user = await request.post("/api/test/users", {
data: {
email: `e2e_${Date.now()}@example.com`,
password: "testpass123",
role: "customer",
},
});
const { id: userId, token } = await user.json();
const product = await request.post("/api/test/products", {
data: { name: "Test Product", price: 29.99, stock: 100 },
});
const { id: productId } = await product.json();
return { userId, token, productId };
}
export async function cleanupTestData(request: APIRequestContext, userId: string) {
await request.delete(`/api/test/users/${userId}`);
await request.delete(`/api/test/products/test-only`);
}
// playwright.config.ts — mobile project
export default defineConfig({
projects: [
{
name: "chromium-desktop",
use: { viewport: { width: 1280, height: 720 } },
},
{
name: "chromium-mobile",
use: {
...devices["Pixel 5"],
isMobile: true,
},
},
{
name: "safari-mobile",
use: {
...devices["iPhone 13"],
isMobile: true,
},
},
],
});
page.waitFor — use auto-waiting locatorsquality-visual-testing for visual regression setup alongside E2E tests.
quality-contract-testing for API contract verification.
Carry forward: test config, page objects, CI pipeline config.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
| Decision Point | Option A | Option B | Decision Criteria |
|---|---|---|---|
| Application type | Web SPA → Playwright/Cypress | Mobile → Detox/Appium | Platform, dev team language |
| Language preference | JavaScript/TypeScript (common) | Python (Playwright + pytest) | Team expertise, existing codebase |
| CI integration | Cloud service (BrowserStack/Sauce) | Self-hosted (Selenium Grid) | Budget, compliance, scale |
| Reporting | Built-in (Playwright HTML) | Third-party (Allure/ReportPortal) | Team preference, existing toolchain |