diff --git a/.gitignore b/.gitignore index 4ca0f808..9d7cf88d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,8 @@ cypress/screenshots/* # System files .DS_Store Thumbs.db + +# Playwright +/playwright-report/ +/playwright/.cache/ +/test-results/ diff --git a/README.md b/README.md index 35324d25..97f3aad7 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,15 @@ Execute `npm run generate-json-api` to generate documentation for any changes in `npm run test` +#### Run Playwright tests + +Playwright tests provide component-level integration and UI interaction testing. + +`npm run playwright` (headless run) or you can open Playwright UI mode using `npm run playwright:ui` + +For more details, see the [Playwright testing approach documentation](docs/testing/playwright-approach.md). + + #### Run Cypress e2e tests `npm run cypress:run` (headless run) or you can open Cypress tests using `npm run cypress:open` diff --git a/docs/testing/playwright-approach.md b/docs/testing/playwright-approach.md new file mode 100644 index 00000000..34e33b1a --- /dev/null +++ b/docs/testing/playwright-approach.md @@ -0,0 +1,321 @@ +# Playwright Testing Approach + +## Overview + +This document outlines the approach for implementing Playwright tests for the CPS Shared UI component library. Playwright has been chosen as a complementary testing tool to provide robust end-to-end (E2E) and integration testing capabilities alongside our existing Jest unit tests and Cypress E2E tests. + +## Runner Choice & Rationale + +### Why Playwright? + +We selected Playwright for the following reasons: + +1. **Modern Architecture**: Playwright provides a modern, actively maintained testing framework with excellent TypeScript support. + +2. **Multi-Browser Support**: Native support for Chromium, Firefox, and WebKit, ensuring cross-browser compatibility. + +3. **Reliability**: Playwright's auto-waiting and retry mechanisms reduce flakiness in tests. + +4. **Performance**: Tests run in parallel by default, providing faster test execution. + +5. **Developer Experience**: + - Excellent debugging tools including UI mode and trace viewer + - Built-in test generation and codegen tools + - Rich assertion library with async/await support + +6. **Complementary to Existing Tools**: + - **Jest**: Continues to handle unit tests for isolated component logic + - **Cypress**: Remains for full E2E workflow testing + - **Playwright**: Focuses on component-level integration and UI interaction testing + - **pa11y-ci**: Continues to handle accessibility testing + +## Component Testing Approach + +### Testing Strategy + +Playwright tests in this project focus on: + +1. **Component Integration**: Testing how components render and behave in the actual documentation/composition application +2. **User Interactions**: Validating click handlers, input changes, and other user interactions +3. **Visual Verification**: Ensuring components are visible and properly styled +4. **Accessibility**: Complementing pa11y-ci with interactive accessibility testing + +### Component Mounting Approach + +Rather than using component testing (which would require experimental packages), we leverage the existing **composition application** as a test harness: + +- Tests navigate to actual component pages (e.g., `/button`, `/chip`) +- Components are tested in their real rendering context +- This approach: + - Requires no additional mounting infrastructure + - Tests components as users see them + - Validates the documentation app alongside components + - Simplifies setup and maintenance + +## Directory Structure + +``` +cps-shared-ui/ +├── playwright/ # Playwright test directory +│ ├── components/ # Component-level tests +│ │ ├── cps-button.spec.ts # Button component tests +│ │ ├── cps-chip.spec.ts # Chip component tests +│ │ └── ... # Additional component tests +│ └── integration/ # Integration tests (future) +│ └── ... +├── playwright.config.ts # Playwright configuration +├── docs/ +│ └── testing/ +│ └── playwright-approach.md # This document +└── package.json # Updated with Playwright scripts +``` + +### File Naming Conventions + +- Component tests: `cps-{component-name}.spec.ts` +- Integration tests: `{feature-name}.spec.ts` +- Use `.spec.ts` extension (consistent with Jest) + +## Example Tests + +### Button Component Test + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('CPS Button Component', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/button'); + }); + + test('should display button with label', async ({ page }) => { + const button = page.locator('cps-button').filter({ hasText: 'Normal button' }).first(); + await expect(button).toBeVisible(); + }); + + test('should be clickable and emit click event', async ({ page }) => { + const button = page.locator('cps-button').filter({ hasText: 'Normal button' }).first(); + await button.locator('button').click(); + await expect(button.locator('button')).toBeEnabled(); + }); +}); +``` + +### Chip Component Test + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('CPS Chip Component', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/chip'); + }); + + test('should display chip with label', async ({ page }) => { + const chip = page.locator('cps-chip').first(); + await expect(chip).toBeVisible(); + }); + + test('should display closable chip with close button', async ({ page }) => { + const closableChip = page.locator('cps-chip') + .filter({ has: page.locator('.cps-chip-close-icon') }) + .first(); + await expect(closableChip).toBeVisible(); + }); +}); +``` + +## Test Patterns & Best Practices + +### Locator Strategies + +1. **Semantic Selectors**: Use component selectors (`cps-button`, `cps-chip`) +2. **Text-based**: Filter by visible text for better readability +3. **Data Attributes**: Use `data-testid` when semantic selectors aren't sufficient +4. **Class Selectors**: For internal component structure (use sparingly) + +### Async/Await Pattern + +All Playwright actions are asynchronous: +```typescript +await page.goto('/button'); +await button.click(); +await expect(element).toBeVisible(); +``` + +### Test Organization + +- Group related tests with `test.describe()` +- Use `test.beforeEach()` for common setup +- Keep tests focused on single behaviors +- Aim for descriptive test names + +## Visual Testing Plan + +### Current Approach + +Visual testing is handled through: +1. Playwright's built-in screenshot capabilities (on-demand) +2. Manual visual review during development +3. Existing accessibility checks via pa11y-ci + +### Future Enhancements + +Potential visual testing improvements: + +1. **Snapshot Testing**: Add visual regression testing using Playwright's screenshot comparison +2. **Percy Integration**: Consider Percy or similar visual testing platforms +3. **Responsive Testing**: Add viewport variations for mobile/tablet/desktop +4. **Theme Testing**: Validate components across different color schemes + +Implementation recommendations: +```typescript +// Example: Visual regression test (future) +test('button should match visual snapshot', async ({ page }) => { + await page.goto('/button'); + await expect(page).toHaveScreenshot('button-page.png'); +}); +``` + +## Running Tests + +### Local Development + +```bash +# Run all Playwright tests +npm run playwright + +# Run tests in UI mode (interactive) +npm run playwright:ui + +# Run tests in headed mode (see browser) +npm run playwright:headed + +# View test report +npm run playwright:report +``` + +### Debugging + +```bash +# Run in UI mode for debugging +npm run playwright:ui + +# Run specific test file +npx playwright test playwright/components/cps-button.spec.ts + +# Run with debug mode +PWDEBUG=1 npx playwright test +``` + +## CI Integration Approach + +### GitHub Actions Workflow + +Add a new job to `.github/workflows/cps-shared-ui-checkers.yml`: + +```yaml +playwright: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + + - name: Run Playwright tests + run: npm run playwright + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 +``` + +### CI Configuration + +The `playwright.config.ts` already includes CI-specific settings: + +- **Retries**: Tests retry 2 times on CI (0 locally) +- **Workers**: Single worker on CI for stability +- **Web Server**: Auto-starts dev server before tests +- **Fail on `.only`**: Prevents accidentally committed focused tests + +### Test Artifacts + +On CI, Playwright automatically generates: +- HTML test report +- Screenshots on failure +- Test traces for debugging + +These are uploaded as GitHub Actions artifacts for review. + +## Migration Strategy + +### Phase 1: PoC (Current) +- ✅ Install and configure Playwright +- ✅ Create 2 example tests (button, chip) +- ✅ Document approach +- ✅ Verify local test execution + +### Phase 2: Expand Coverage +- Add tests for 5-10 most critical components +- Establish test patterns and conventions +- Add CI integration +- Train team on Playwright usage + +### Phase 3: Full Integration +- Comprehensive component coverage +- Visual regression testing +- Performance benchmarks +- Integration with existing testing workflows + +## Maintenance & Best Practices + +### When to Use Each Testing Tool + +| Tool | Use Case | +|------|----------| +| **Jest** | Unit tests, isolated component logic, service testing | +| **Playwright** | Component integration, UI interactions, visual verification | +| **Cypress** | Full E2E workflows, multi-page user journeys | +| **pa11y-ci** | Automated accessibility compliance checking | + +### Code Review Checklist + +- [ ] Tests use semantic locators +- [ ] Test names clearly describe behavior +- [ ] No hardcoded waits (use Playwright's auto-waiting) +- [ ] Tests are independent (no shared state) +- [ ] Appropriate use of `.first()`, `.nth()` for multiple elements + +### Performance Considerations + +- Run tests in parallel (default) +- Use `fullyParallel: true` for faster execution +- Limit unnecessary page navigations +- Reuse test context when possible + +## Resources + +- [Playwright Documentation](https://playwright.dev) +- [Playwright Best Practices](https://playwright.dev/docs/best-practices) +- [Playwright Test Generator](https://playwright.dev/docs/codegen) +- [Angular Testing Guide](https://angular.dev/guide/testing) + +## Conclusion + +Playwright provides a robust, modern testing solution that complements our existing test infrastructure. By leveraging the composition application as a test harness, we can efficiently test component behavior without complex setup. This approach balances comprehensive testing with maintainability and developer experience. diff --git a/package-lock.json b/package-lock.json index 1defc825..42981cba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@angular-devkit/build-angular": "^20.3.9", "@angular/cli": "~20.3.9", "@angular/compiler-cli": "^20.3.10", + "@playwright/test": "^1.58.0", "@types/express": "^4.17.25", "@types/jest": "^29.5.14", "@types/node": "^22.10.10", @@ -5738,6 +5739,21 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.58.0", + "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@primeuix/styled": { "version": "0.7.4", "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", @@ -18026,6 +18042,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.58.0", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/portscanner": { "version": "2.2.0", "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", diff --git a/package.json b/package.json index e471e898..bc695470 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "test:a11y:local": "npm run start & sleep 10 && npm run test:a11y:summary && kill %1", "cypress:open": "cypress open", "cypress:run": "cypress run", + "playwright": "playwright test", + "playwright:ui": "playwright test --ui", + "playwright:headed": "playwright test --headed", + "playwright:report": "playwright show-report", "format": "prettier --write \"**/*.{ts,html}\"" }, "private": true, @@ -49,6 +53,7 @@ "@angular-devkit/build-angular": "^20.3.9", "@angular/cli": "~20.3.9", "@angular/compiler-cli": "^20.3.10", + "@playwright/test": "^1.58.0", "@types/express": "^4.17.25", "@types/jest": "^29.5.14", "@types/node": "^22.10.10", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..f595952c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,41 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:4200', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start', + url: 'http://localhost:4200', + reuseExistingServer: true, + timeout: 60000, + }, +}); diff --git a/playwright/README.md b/playwright/README.md new file mode 100644 index 00000000..8b38791b --- /dev/null +++ b/playwright/README.md @@ -0,0 +1,63 @@ +# Playwright Tests + +This directory contains Playwright tests for the CPS Shared UI component library. + +## Structure + +``` +playwright/ +├── components/ # Component-level tests +│ ├── cps-button.spec.ts +│ ├── cps-chip.spec.ts +│ └── ... +└── integration/ # Integration tests (future) +``` + +## Running Tests + +```bash +# Run all tests +npm run playwright + +# Run tests in UI mode (interactive debugging) +npm run playwright:ui + +# Run tests in headed mode (see browser) +npm run playwright:headed + +# View test report +npm run playwright:report + +# Run specific test file +npx playwright test playwright/components/cps-button.spec.ts + +# Debug tests +PWDEBUG=1 npx playwright test +``` + +## Writing Tests + +Tests should follow these patterns: + +1. **Use semantic selectors**: Target components by their selectors (e.g., `cps-button`) +2. **Filter by visible text**: Use `.filter({ hasText: 'text' })` for readability +3. **Keep tests focused**: Each test should verify a single behavior +4. **Use descriptive names**: Test names should clearly describe what is being tested + +Example: + +```typescript +test('should display button with label', async ({ page }) => { + await page.goto('/button'); + const button = page.locator('cps-button').filter({ hasText: 'Normal button' }).first(); + await expect(button).toBeVisible(); +}); +``` + +## Test Coverage + +Currently covered components: +- ✅ Button +- ✅ Chip + +See `/docs/testing/playwright-approach.md` for comprehensive documentation. diff --git a/playwright/components/cps-button.spec.ts b/playwright/components/cps-button.spec.ts new file mode 100644 index 00000000..b2a8b70a --- /dev/null +++ b/playwright/components/cps-button.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; + +test.describe('CPS Button Component', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/button'); + }); + + test('should display button with label', async ({ page }) => { + // Find a button by its label text + const button = page.locator('cps-button').filter({ hasText: 'Normal button' }).first(); + + await expect(button).toBeVisible(); + await expect(button.locator('.cps-button__text')).toBeVisible(); + }); + + test('should be clickable and emit click event', async ({ page }) => { + // Find a clickable button + const button = page.locator('cps-button').filter({ hasText: 'Normal button' }).first(); + const buttonElement = button.locator('button'); + + await expect(buttonElement).toBeEnabled(); + await buttonElement.click(); + + // Verify the button is still enabled after click + await expect(buttonElement).toBeEnabled(); + }); + + test('should display disabled button', async ({ page }) => { + // Find disabled buttons on the page + const disabledButton = page.locator('cps-button button[disabled]').first(); + + await expect(disabledButton).toBeDisabled(); + }); + + test('should display button with icon', async ({ page }) => { + // Find a button with an icon + const buttonWithIcon = page.locator('cps-button').filter({ has: page.locator('cps-icon') }).first(); + + await expect(buttonWithIcon).toBeVisible(); + await expect(buttonWithIcon.locator('cps-icon')).toBeVisible(); + }); + + test('should apply solid button type', async ({ page }) => { + // Check for solid buttons (default) + const solidButton = page.locator('cps-button .cps-button--solid').first(); + await expect(solidButton).toBeVisible(); + + // Check that button has proper classes + const buttonClasses = await solidButton.getAttribute('class'); + expect(buttonClasses).toContain('cps-button'); + expect(buttonClasses).toContain('cps-button--solid'); + }); + + test('should display different button sizes', async ({ page }) => { + // Check for buttons with large size + const largeButton = page.locator('cps-button .cps-button--large').first(); + await expect(largeButton).toBeVisible(); + + const largeClasses = await largeButton.getAttribute('class'); + expect(largeClasses).toContain('cps-button--large'); + + // Check for buttons with small size + const smallButton = page.locator('cps-button .cps-button--small').first(); + await expect(smallButton).toBeVisible(); + + const smallClasses = await smallButton.getAttribute('class'); + expect(smallClasses).toContain('cps-button--small'); + }); + + test('should display loading state', async ({ page }) => { + // Find a button in loading state + const loadingButton = page.locator('cps-button').filter({ has: page.locator('cps-progress-circular') }).first(); + + await expect(loadingButton).toBeVisible(); + await expect(loadingButton.locator('cps-progress-circular')).toBeVisible(); + }); +}); diff --git a/playwright/components/cps-chip.spec.ts b/playwright/components/cps-chip.spec.ts new file mode 100644 index 00000000..eed317f7 --- /dev/null +++ b/playwright/components/cps-chip.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +test.describe('CPS Chip Component', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/chip'); + }); + + test('should display chip with label', async ({ page }) => { + // Find a chip by its label + const chip = page.locator('cps-chip').first(); + + await expect(chip).toBeVisible(); + await expect(chip.locator('.cps-chip-label')).toBeVisible(); + }); + + test('should display closable chip with close button', async ({ page }) => { + // Find closable chip with close icon + const closableChips = page.locator('cps-chip').filter({ has: page.locator('.cps-chip-close-icon') }); + + // Verify at least one closable chip exists + await expect(closableChips.first()).toBeVisible(); + await expect(closableChips.first().locator('.cps-chip-close-icon')).toBeVisible(); + }); + + test('should display chip with icon', async ({ page }) => { + // Find chips with icons + const chipsWithIcon = page.locator('cps-chip').filter({ has: page.locator('cps-icon') }); + + // Verify at least one chip with icon exists + await expect(chipsWithIcon.first()).toBeVisible(); + await expect(chipsWithIcon.first().locator('cps-icon')).toBeVisible(); + }); + + test('should display multiple chips', async ({ page }) => { + // Count chips on the page + const chips = page.locator('cps-chip'); + const chipCount = await chips.count(); + + expect(chipCount).toBeGreaterThan(0); + }); +});