diff --git a/Claude/agents/e2e-runner.md b/Claude/agents/e2e-runner.md index 6f31aa3..9d7c11f 100644 --- a/Claude/agents/e2e-runner.md +++ b/Claude/agents/e2e-runner.md @@ -58,6 +58,7 @@ npx playwright show-report # View HTML report ### 2. Create - Use Page Object Model (POM) pattern - Prefer `data-testid` locators over CSS/XPath +- Prefer locator-based Playwright actions over raw `page.click()` and `page.fill()` - Add assertions at key steps - Capture screenshots at critical points - Use proper waits (never `waitForTimeout`) @@ -80,7 +81,7 @@ npx playwright show-report # View HTML report ```typescript // Quarantine -test('flaky: market search', async ({ page }) => { +test('flaky: should show market search results', async ({ page }) => { test.fixme(true, 'Flaky - Issue #123') }) @@ -90,6 +91,11 @@ test('flaky: market search', async ({ page }) => { Common causes: race conditions (use auto-wait locators), network timing (wait for response), animation timing (wait for `networkidle`). +## Coverage Expectations + +- Browser: Chromium, Firefox, WebKit for critical web flows +- Mobile web: at least one Android-sized and one iPhone-sized device profile when touch behavior matters + ## Success Metrics - All critical journeys passing (100%) diff --git a/Claude/rules/common/testing.md b/Claude/rules/common/testing.md index 416c1c2..dc17297 100644 --- a/Claude/rules/common/testing.md +++ b/Claude/rules/common/testing.md @@ -33,7 +33,7 @@ MANDATORY workflow: Prefer Arrange-Act-Assert structure for tests: ```typescript -test('calculates similarity correctly', () => { +test('should calculate similarity correctly', () => { // Arrange const vector1 = [1, 0, 0] const vector2 = [0, 1, 0] @@ -50,8 +50,11 @@ test('calculates similarity correctly', () => { Use descriptive names that explain the behavior under test: +- Prefer `should` phrasing for test names. +- Use present-tense behavior statements that read clearly in test output. + ```typescript -test('returns empty array when no markets match query', () => {}) -test('throws error when API key is missing', () => {}) -test('falls back to substring search when Redis is unavailable', () => {}) +test('should return empty array when no markets match query', () => {}) +test('should throw error when API key is missing', () => {}) +test('should fall back to substring search when Redis is unavailable', () => {}) ``` diff --git a/Claude/rules/web/testing.md b/Claude/rules/web/testing.md index 6bf5812..095234f 100644 --- a/Claude/rules/web/testing.md +++ b/Claude/rules/web/testing.md @@ -27,19 +27,21 @@ - Minimum: Chrome, Firefox, Safari - Test scrolling, motion, and fallback behavior +- Use Playwright projects for Chromium, Firefox, and WebKit coverage ### 5. Responsive - Test 320, 375, 768, 1024, 1440, 1920 - Verify no overflow - Verify touch interactions +- Add at least one Android-sized and one iPhone-sized mobile-web project when touch behavior matters ## E2E Shape ```ts import { test, expect } from '@playwright/test'; -test('landing hero loads', async ({ page }) => { +test('should load landing hero', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1')).toBeVisible(); }); diff --git a/Claude/skills/e2e-testing/SKILL.md b/Claude/skills/e2e-testing/SKILL.md index 0563199..619609f 100644 --- a/Claude/skills/e2e-testing/SKILL.md +++ b/Claude/skills/e2e-testing/SKILL.md @@ -26,6 +26,19 @@ tests/ ├── fixtures/ │ ├── auth.ts │ └── data.ts +├── pageObjects/ +│ ├── components/ +│ │ ├── base.component.ts +│ │ └── header.component.ts +│ └── pages/ +│ ├── base.page.ts +│ └── items.page.ts +├── types/ +│ ├── searchData.ts +│ └── loginData.ts +├── utils/ +│ ├── browserHelpers.ts +│ └── dateUtils.ts └── playwright.config.ts ``` @@ -34,41 +47,64 @@ tests/ ```typescript import { Page, Locator } from '@playwright/test' +export class Header { + readonly profileMenuButton: Locator + + constructor(private readonly page: Page) { + this.profileMenuButton = page.getByTestId('profile-menu-button') + } + + async openProfileMenu(): Promise { + await this.profileMenuButton.click() + } +} + export class ItemsPage { - readonly page: Page readonly searchInput: Locator readonly itemCards: Locator readonly createButton: Locator + readonly header: Header - constructor(page: Page) { - this.page = page - this.searchInput = page.locator('[data-testid="search-input"]') - this.itemCards = page.locator('[data-testid="item-card"]') - this.createButton = page.locator('[data-testid="create-btn"]') + constructor(private readonly page: Page) { + this.searchInput = page.getByTestId('search-input') + this.itemCards = page.getByTestId('item-card') + this.createButton = page.getByTestId('create-btn') + this.header = new Header(page) } - async goto() { + async goto(): Promise { await this.page.goto('/items') - await this.page.waitForLoadState('networkidle') + await this.searchInput.waitFor({ state: 'visible', timeout: 5000 }) } - async search(query: string) { + async search(query: string): Promise { await this.searchInput.fill(query) await this.page.waitForResponse(resp => resp.url().includes('/api/search')) - await this.page.waitForLoadState('networkidle') + await this.itemCards.first().waitFor({ state: 'visible', timeout: 5000 }) + } + + async openCreateFlow(): Promise { + await this.createButton.click() } - async getItemCount() { - return await this.itemCards.count() + async getItemCount(): Promise { + return this.itemCards.count() } } ``` +### POM Rules + +- Exported page object methods should have explicit return types. +- Prefer `getByTestId`, `getByRole`, and `getByLabel` before CSS selectors. +- Keep repeated UI fragments such as headers, dialogs, or sidebars in reusable component objects. +- Do not hide assertions inside page objects; keep high-signal business assertions visible in the test. + ## Test Structure ```typescript import { test, expect } from '@playwright/test' -import { ItemsPage } from '../../pages/ItemsPage' +import { ItemsPage } from '../../pageObjects/pages/items.page' test.describe('Item Search', () => { let itemsPage: ItemsPage @@ -91,12 +127,45 @@ test.describe('Item Search', () => { test('should handle no results', async ({ page }) => { await itemsPage.search('xyznonexistent123') - await expect(page.locator('[data-testid="no-results"]')).toBeVisible() + await expect(page.getByTestId('no-results')).toBeVisible() expect(await itemsPage.getItemCount()).toBe(0) }) }) ``` +## Fixtures, Types, and Utils Setup + +### Fixtures + +```typescript +import { LoginData } from '../types/loginData' + +export const auth: LoginData = { + emailAddress: 'testing@testmail.com', + password: 'Password123', +} +``` + +### Types + +```typescript +export interface LoginData { + emailAddress: string; + password: string; +} +``` + +### Utils + +```typescript +export class DateUtils { + static formatToISO(date: Date): string { + return date.toISOString().split('T')[0] + } + // more functions... +} +``` + ## Playwright Configuration ```typescript @@ -125,7 +194,8 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, - { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, + { name: 'mobile-android', use: { ...devices['Pixel 5'] } }, + { name: 'mobile-ios', use: { ...devices['iPhone 13'] } }, ], webServer: { command: 'npm run dev', @@ -141,12 +211,12 @@ export default defineConfig({ ### Quarantine ```typescript -test('flaky: complex search', async ({ page }) => { +test('flaky: should perform a complex search', async ({ page }) => { test.fixme(true, 'Flaky - Issue #123') // test code... }) -test('conditional skip', async ({ page }) => { +test('flaky in CI: should show filtered results', async ({ page }) => { test.skip(process.env.CI, 'Flaky in CI - Issue #123') // test code... }) @@ -167,7 +237,7 @@ npx playwright test tests/search.spec.ts --retries=3 await page.click('[data-testid="button"]') // Good: auto-wait locator -await page.locator('[data-testid="button"]').click() +await page.getByTestId('button').click() ``` **Network timing:** @@ -185,9 +255,9 @@ await page.waitForResponse(resp => resp.url().includes('/api/data')) await page.click('[data-testid="menu-item"]') // Good: wait for stability -await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' }) -await page.waitForLoadState('networkidle') -await page.locator('[data-testid="menu-item"]').click() +const menuItem = page.getByTestId('menu-item') +await menuItem.waitFor({ state: 'visible', timeout: 5000 }) +await menuItem.click() ``` ## Artifact Management @@ -197,7 +267,7 @@ await page.locator('[data-testid="menu-item"]').click() ```typescript await page.screenshot({ path: 'artifacts/after-login.png' }) await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true }) -await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' }) +await page.getByTestId('chart').screenshot({ path: 'artifacts/chart.png' }) ``` ### Traces @@ -280,7 +350,7 @@ jobs: ## Wallet / Web3 Testing ```typescript -test('wallet connection', async ({ page, context }) => { +test('should connect wallet', async ({ page, context }) => { // Mock wallet provider await context.addInitScript(() => { window.ethereum = { @@ -294,33 +364,33 @@ test('wallet connection', async ({ page, context }) => { }) await page.goto('/') - await page.locator('[data-testid="connect-wallet"]').click() - await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234') + await page.getByTestId('connect-wallet').click() + await expect(page.getByTestId('wallet-address')).toContainText('0x1234') }) ``` ## Financial / Critical Flow Testing ```typescript -test('trade execution', async ({ page }) => { +test('should execute trade', async ({ page }) => { // Skip on production — real money test.skip(process.env.NODE_ENV === 'production', 'Skip on production') await page.goto('/markets/test-market') - await page.locator('[data-testid="position-yes"]').click() - await page.locator('[data-testid="trade-amount"]').fill('1.0') + await page.getByTestId('position-yes').click() + await page.getByTestId('trade-amount').fill('1.0') // Verify preview - const preview = page.locator('[data-testid="trade-preview"]') + const preview = page.getByTestId('trade-preview') await expect(preview).toContainText('1.0') // Confirm and wait for blockchain - await page.locator('[data-testid="confirm-trade"]').click() + await page.getByTestId('confirm-trade').click() await page.waitForResponse( resp => resp.url().includes('/api/trade') && resp.status() === 200, { timeout: 30000 } ) - await expect(page.locator('[data-testid="trade-success"]')).toBeVisible() + await expect(page.getByTestId('trade-success')).toBeVisible() }) ``` diff --git a/Claude/skills/tdd-workflow/SKILL.md b/Claude/skills/tdd-workflow/SKILL.md index 76aaaa1..b825a69 100644 --- a/Claude/skills/tdd-workflow/SKILL.md +++ b/Claude/skills/tdd-workflow/SKILL.md @@ -76,19 +76,19 @@ For each user journey, create comprehensive test cases: ```typescript describe('Semantic Search', () => { - it('returns relevant markets for query', async () => { + it('should return relevant markets for query', async () => { // Test implementation }) - it('handles empty query gracefully', async () => { + it('should handle empty query gracefully', async () => { // Test edge case }) - it('falls back to substring search when Redis unavailable', async () => { + it('should fall back to substring search when Redis unavailable', async () => { // Test fallback behavior }) - it('sorts results by similarity score', async () => { + it('should sort results by similarity score', async () => { // Test sorting logic }) }) @@ -177,12 +177,12 @@ import { render, screen, fireEvent } from '@testing-library/react' import { Button } from './Button' describe('Button Component', () => { - it('renders with correct text', () => { + it('should render the button with correct text', () => { render() expect(screen.getByText('Click me')).toBeInTheDocument() }) - it('calls onClick when clicked', () => { + it('should call onClick when clicked', () => { const handleClick = jest.fn() render() @@ -191,7 +191,7 @@ describe('Button Component', () => { expect(handleClick).toHaveBeenCalledTimes(1) }) - it('is disabled when disabled prop is true', () => { + it('should disable the button when disabled prop is true', () => { render() expect(screen.getByRole('button')).toBeDisabled() }) @@ -233,22 +233,22 @@ describe('GET /api/markets', () => { ```typescript import { test, expect } from '@playwright/test' -test('user can search and filter markets', async ({ page }) => { +test('should let user search and filter markets', async ({ page }) => { // Navigate to markets page await page.goto('/') - await page.click('a[href="/markets"]') + await page.locator('a[href="/markets"]').click() // Verify page loaded await expect(page.locator('h1')).toContainText('Markets') // Search for markets - await page.fill('input[placeholder="Search markets"]', 'election') + await page.locator('input[placeholder="Search markets"]').fill('election') - // Wait for debounce and results - await page.waitForTimeout(600) + // Wait for the filtered result set to settle + const results = page.getByTestId('market-card') + await results.first().waitFor({ state: 'visible', timeout: 5000 }) // Verify search results displayed - const results = page.locator('[data-testid="market-card"]') await expect(results).toHaveCount(5, { timeout: 5000 }) // Verify results contain search term @@ -256,23 +256,23 @@ test('user can search and filter markets', async ({ page }) => { await expect(firstResult).toContainText('election', { ignoreCase: true }) // Filter by status - await page.click('button:has-text("Active")') + await page.locator('button:has-text("Active")').click() // Verify filtered results await expect(results).toHaveCount(3) }) -test('user can create a new market', async ({ page }) => { +test('should let user create a new market', async ({ page }) => { // Login first await page.goto('/creator-dashboard') // Fill market creation form - await page.fill('input[name="name"]', 'Test Market') - await page.fill('textarea[name="description"]', 'Test description') - await page.fill('input[name="endDate"]', '2025-12-31') + await page.locator('input[name="name"]').fill('Test Market') + await page.locator('textarea[name="description"]').fill('Test description') + await page.locator('input[name="endDate"]').fill('2025-12-31') // Submit form - await page.click('button[type="submit"]') + await page.locator('button[type="submit"]').click() // Verify success message await expect(page.locator('text=Market created successfully')).toBeVisible()