算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-testing【设计与创意】Use this skill when the user says 'frontend testing', 'component test', 'React testing', 'Vue testing', 'Angular testing', 'testing library', 'Cypress', 'Playwright', 'snapshot test', 'what to test frontend', or when writing frontend tests. This skill enforces: Testing Library queries by user-facing attributes (getByRole, getByLabelText), userEvent o…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Test user behavior, not implementation. Query elements the way users find them (by role, label, text). UserEvent simulates real interactions. Every component has an accessibility check.
Exact user phrases: "frontend testing", "component test", "React testing", "Vue testing", "Angular testing", "testing library", "Cypress", "Playwright", "snapshot test", "what to test frontend".
Before activating, verify:
No file output. Produces test code as text.
Test file:
describe('{Component}', () => {
it('should {expected behavior} when {condition}', () => {
// render
// interact
// assert
})
})
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Per test: 20 lines.
What are you testing?
|-- Single component logic -->
| UNIT / COMPONENT TEST
| Tool: Vitest / Jest + Testing Library
| Coverage: happy path, empty, loading, error, edge cases
| Assert: rendered output, called handlers, accessibility
|
|-- Multi-component interaction -->
| INTEGRATION TEST
| Tool: Vitest / Jest + Testing Library
| Approach: render parent, mock child API calls with MSW
| Assert: data flows correctly between components
|
|-- Full user journey -->
| E2E TEST
| Tool: Playwright / Cypress
| Coverage: 5-10 critical journeys per app (login, purchase, search)
| Assert: navigation, data persistence, UI flow
|
|-- Visual appearance -->
VISUAL REGRESSION TEST
Tool: Chromatic / Percy / Playwright snapshot
Coverage: changed components only (diff-based)
Assert: pixel-perfect match with baseline
How does the user find the element?
|-- By role + accessible name (button "Submit", link "View details") -->
| getByRole('button', { name: /submit/i })
| Preferred. Matches accessibility tree.
|
|-- By associated label (form fields) -->
| getByLabelText(/email/i)
| Links label to input. Tests accessibility.
|
|-- By text content (headings, paragraphs) -->
| getByText(/order confirmed/i)
| For non-interactive text content.
|
|-- By placeholder (no visible label) -->
| getByPlaceholderText(/search/i)
| Last resort before getByTestId.
|
|-- By test ID (absolutely no other option) -->
getByTestId('order-list')
Brittle. Avoids user-facing queries. Document why.
What does the component depend on?
|-- API calls -->
| MSW (Mock Service Worker) — intercepts at network level
| No module mocking needed, tests use real fetch
|
|-- Third-party libraries (auth, analytics) -->
| Module mock: vi.mock('@auth/client')
| Only for libraries where MSW can't intercept
|
|-- Context / providers -->
| Wrap in test provider with controlled values
| Test with different provider values
|
|-- Feature flags -->
MockFlagClient with specific flag values
Test enabled and disabled states
| Query | When to Use |
|---|---|
| getByRole | Preferred. Finds by ARIA role + accessible name. |
| getByLabelText | Form fields. Links label to input via for/id or aria-labelledby. |
| getByPlaceholderText | Only if no label exists. Last resort before getByTestId. |
| getByText | Non-interactive elements (paragraphs, headings, spans). |
| getByTestId | Absolute last resort. Brittle — avoids user-facing queries. |
describe('UserForm', () => {
const setup = () => {
const onSubmit = vi.fn()
render(<UserForm onSubmit={onSubmit} />)
return { onSubmit }
}
it('should show validation error when email is invalid', async () => {
setup()
const emailInput = screen.getByLabelText(/email/i)
await userEvent.type(emailInput, 'invalid-email')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(screen.getByText(/valid email/i)).toBeInTheDocument()
})
it('should call onSubmit when form is valid', async () => {
const { onSubmit } = setup()
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com')
await userEvent.type(screen.getByLabelText(/name/i), 'John')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
name: 'John',
})
})
it('should have no accessibility violations', async () => {
const { container } = render(<UserForm onSubmit={vi.fn()} />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
test('user can complete checkout flow', async ({ page }) => {
await page.goto('/products')
await page.getByRole('button', { name: /add to cart/i }).first().click()
await page.getByRole('link', { name: /cart/i }).click()
await page.getByRole('button', { name: /checkout/i }).click()
await page.getByLabelText(/email/i).fill('user@example.com')
await page.getByRole('button', { name: /place order/i }).click()
await expect(page.getByText(/order confirmed/i)).toBeVisible()
})
E2E rules:
| Type | Target | What It Covers |
|---|---|---|
| Component tests | >= 70% of components | Each component's states rendered and interacted |
| Integration | >= 50% of feature flows | Multi-component interaction |
| E2E | 100% of critical paths | User journeys |
| Accessibility | 100% of components | axe-core assertions |
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
])
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
it('displays users from API', async () => {
render(<UserList />)
expect(await screen.findByText('Alice')).toBeInTheDocument()
expect(await screen.findByText('Bob')).toBeInTheDocument()
})
// Custom fixture with authenticated state
// fixtures.ts
import { test as base, type Page } from '@playwright/test'
type MyFixtures = {
authenticatedPage: Page
}
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login')
await page.getByLabelText(/email/i).fill('test@example.com')
await page.getByLabelText(/password/i).fill('password123')
await page.getByRole('button', { name: /sign in/i }).click()
await page.waitForURL('/dashboard')
await use(page)
},
})
// Usage
test('user can create invoice', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/invoices/new')
await authenticatedPage.getByLabelText(/amount/i).fill('100')
await authenticatedPage.getByRole('button', { name: /save/i }).click()
await expect(authenticatedPage.getByText(/invoice created/i)).toBeVisible()
})
// API mocking in Playwright
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
baseURL: 'http://localhost:3000',
extraHTTPHeaders: { 'x-test-mode': 'true' },
},
})
// Block third-party requests for faster tests
test.use({
blockURLs: ['https://www.googletagmanager.com/', 'https://cdn.segment.com/'],
})
// Mock API responses per test
test('shows empty state when no orders exist', async ({ page }) => {
await page.route('**/api/orders/**', async (route) => {
await route.fulfill({ json: { orders: [] } })
})
await page.goto('/orders')
await expect(page.getByText(/no orders yet/i)).toBeVisible()
})
// Perceptual diff (Playwright)
import { test, expect } from '@playwright/test'
test('homepage matches snapshot', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // allow minor anti-aliasing differences
fullPage: true,
})
})
// Component-level (Chromatic via Storybook)
// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
const meta = {
component: Button,
parameters: { chromatic: { diffThreshold: 0.2 } },
} satisfies Meta<typeof Button>
export default meta
export const Primary: StoryObj<typeof meta> = {
args: { variant: 'primary', children: 'Click Me' },
}
// CI integration: chromatic detects visual diffs and prompts approval
// React — test hooks with renderHook
import { renderHook, act } from '@testing-library/react'
it('increments counter', () => {
const { result } = renderHook(() => useState(0))
act(() => { result.current[1](result.current[0] + 1) })
expect(result.current[0]).toBe(1)
})
// Vue — test composables with mount
import { mount } from '@vue/test-utils'
it('emits submit event', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[type="email"]').setValue('user@example.com')
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')).toBeTruthy()
})
// Angular — test with TestBed
import { TestBed } from '@angular/core/testing'
it('displays user name', () => {
const fixture = TestBed.createComponent(UserProfileComponent)
fixture.componentInstance.user = { name: 'Alice' }
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Alice')
})
# .github/workflows/test.yml
name: Tests
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx vitest --shard=${{ matrix.shard }}/${{ strategy.job-total }}
# Split 1000 tests across 4 shards: ~250 tests each
strategy:
matrix:
shard: [1, 2, 3, 4]
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --workers=4
// Skip slow tests in watch mode
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
testTimeout: process.env.CI ? 30000 : 10000,
retry: process.env.CI ? 2 : 0, // retry flaky tests in CI
},
})
it('shows loading then data', async () => {
render(<UserList />)
expect(screen.getByRole('status')).toBeInTheDocument() // loading state
expect(await screen.findByRole('list')).toBeInTheDocument() // data loaded
})
it('shows error state on API failure', async () => {
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 })
})
)
render(<UserList />)
expect(await screen.findByText(/failed to load/i)).toBeInTheDocument()
})
// BAD -- tests internal state
expect(counter.state.count).toBe(1)
// GOOD -- tests user-visible behavior
expect(screen.getByText('1')).toBeInTheDocument()
Snapshots fail on every formatting change (Prettier, whitespace, comments). They create false negatives and desensitize developers to real failures. Prefer explicit assertions.
// BAD -- doesn't simulate real browser behavior
fireEvent.change(input, { target: { value: 'test' } })
// GOOD -- simulates real typing
await userEvent.type(input, 'test')
Every getByTestId is a missed opportunity to test accessibility. If you can't find an element by role/label/text, your component likely has accessibility issues.
Tests that share mutable state are flaky and order-dependent. Use beforeEach to reset state.
Network-dependent tests fail intermittently. Use retries for flaky tests and stable selectors (getByRole, getByLabelText) over CSS/XPath.
// Playwright auto-retry pattern
await expect(page.getByText(/order confirmed/i)).toBeVisible({ timeout: 10000 })
Mocking too many dependencies creates tests that pass but don't test real behavior. MSW at network level is preferred; avoid mocking utilities, formatters, and framework internals.
| Tool | Type | Speed | Purpose |
|---|---|---|---|
| Vitest | Unit/Component | Fastest | Component tests, pure logic |
| Jest | Unit/Component | Fast | Legacy projects |
| Testing Library | Utility | N/A | Query patterns (used with Vitest/Jest) |
| Playwright | E2E | Slowest | Critical user journeys |
| Cypress | E2E | Slow | Critical user journeys, debugging |
| MSW | Mocking | N/A | Network-level API mocking |
| Chromatic | Visual | Slow | Visual regression (diff-based) |
axe accessibility check adds ~100-500ms per testtest.concurrent for independent integration tests reduces suite timegetByRole tests that the component is semantically correctgetByLabelText tests form field labelinguserEvent.tab() tests keyboard navigation and focus orderprefers-reduced-motion: reduce if animation is presentNo artifact produced. Next skill: {framework}-architecture or code-review. Carry forward: testing framework, E2E setup, coverage targets.