diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index a1d6590..b45cd73 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -29,11 +29,9 @@ jobs: - name: Install dependencies run: npm ci - working-directory: frontend - name: Detect package scripts id: pkg - working-directory: frontend run: | node -e ' const fs = require("fs"); @@ -41,26 +39,42 @@ jobs: const s = pkg.scripts || {}; const out = [ "has_lint=" + ("lint" in s), - "has_type_check=" + ("type-check" in s), + "has_typecheck=" + ("typecheck" in s), "has_build=" + ("build" in s), - "has_test=" + ("test" in s) + "has_test=" + ("test" in s), + "has_e2e=" + ("test:e2e" in s) ].join("\n"); fs.appendFileSync(process.env.GITHUB_OUTPUT, out + "\n");' + - name: Run lint if: steps.pkg.outputs.has_lint == 'true' run: npm run lint - working-directory: frontend - name: Run TypeScript type-check - if: steps.pkg.outputs.has_type_check == 'true' - run: npm run type-check - working-directory: frontend + if: steps.pkg.outputs.has_typecheck == 'true' + run: npm run typecheck - - name: Run tests + - name: Run unit tests if: steps.pkg.outputs.has_test == 'true' run: npm test - working-directory: frontend - name: Build app run: npm run build - working-directory: frontend + + - name: Install Playwright browsers + if: steps.pkg.outputs.has_e2e == 'true' + run: npx playwright install --with-deps chromium + + - name: Run E2E tests against built app + if: steps.pkg.outputs.has_e2e == 'true' + run: npm run test:e2e + env: + CI: true + + - name: Upload Playwright report + if: failure() && steps.pkg.outputs.has_e2e == 'true' + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: frontend/playwright-report/ + retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b69f7..9dc2298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Playwright E2E suite for the reference UI (`frontend/playwright.config.ts`, `frontend/e2e/`) targeting `localhost:3000`, with `test:e2e` / `test:e2e:ui` scripts and Frontend CI running E2E against the built Next.js app - Initial MVP implementation of Bridgelet for ephemeral Stellar accounts - **bridgelet-core**: Soroban smart contracts for on-chain account restrictions, sweep logic, and event emission ([bridgelet-core](https://github.com/bridgelet-org/bridgelet-core)) - **bridgelet-sdk**: NestJS backend SDK for account lifecycle management, claim authentication, and webhook system ([bridgelet-sdk](https://github.com/bridgelet-org/bridgelet-sdk)) diff --git a/TESTING.md b/TESTING.md index be95a86..531adf9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -43,7 +43,7 @@ The table below maps each part of the codebase to the testing tools and the curr |---|---|---|---| | Frontend components | Vitest + React Testing Library | Unit / component rendering | 🔲 Planned | | Frontend API layer | MSW v2 | Network mock in dev and tests | ✅ Mock handlers implemented | -| Frontend E2E flows | Playwright | Full browser user journeys | 🔲 Planned | +| Frontend E2E flows | Playwright | Full browser user journeys | ✅ Configured (smoke suite) | | Frontend visual regression | Storybook + Chromatic | Component story snapshots | 🔲 Planned | | Frontend performance | Lighthouse CI | Core Web Vitals, accessibility score | 🔲 Planned | | Mobile unit tests | Jest + jest-expo | Component and utility logic | ⚠️ Runner configured, no test files yet | @@ -210,121 +210,64 @@ npm run test:coverage ### Playwright E2E Tests -Playwright drives a real browser against the running Next.js dev or preview server. Use it for critical user journeys: sender flow, claim flow, and error paths. +Playwright drives a real browser against `http://localhost:3000`. Use it for critical user journeys: navigation, sender flow, claim flow, and error paths. -#### Setup +#### Location + +| Path | Purpose | +|---|---| +| `frontend/playwright.config.ts` | Base config (`baseURL` → `localhost:3000`) | +| `frontend/e2e/*.spec.ts` | Browser specs (smoke suite today) | +| `docs/testing/E2E_GUIDELINES.md` | Selector and isolation standards | + +#### One-time browser install ```bash cd frontend -npm install --save-dev @playwright/test npx playwright install --with-deps chromium ``` -Create `frontend/playwright.config.ts`: - -```ts -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - reporter: process.env.CI ? 'github' : 'list', - use: { - baseURL: 'http://localhost:3000', - trace: 'on-first-retry', - }, - projects: [ - { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, - ], - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - reuseExistingServer: !process.env.CI, - }, -}); -``` +#### Running Playwright tests -#### Writing tests +```bash +cd frontend -Place test files in `frontend/e2e/`. MSW service worker is active in the dev server, so network calls are intercepted automatically. +# Headless — starts `next dev` automatically (or reuses a running server) +npm run test:e2e -```ts -// e2e/send-flow.spec.ts -import { test, expect } from '@playwright/test'; +# Interactive UI mode +npm run test:e2e:ui -test('sender can complete the send form', async ({ page }) => { - await page.goto('/send'); +# Single file +npx playwright test e2e/home.spec.ts - // connect wallet step - await page.getByRole('button', { name: /connect wallet/i }).click(); - await expect(page.getByText(/wallet connected/i)).toBeVisible(); +# Debug mode (pauses on each step) +npx playwright test --debug +``` - // fill details - await page.getByLabel('Amount').fill('10'); - await page.getByRole('button', { name: /continue/i }).click(); +Locally, `webServer` runs `npm run dev` and reuses an existing server when present. In CI (`CI=true`), it runs `npm run start` against the **built** Next.js app (the workflow runs `npm run build` first). - // confirm step - await expect(page.getByText(/confirm/i)).toBeVisible(); - await page.getByRole('button', { name: /send/i }).click(); +#### Writing tests - // share prompt - await expect(page.getByText(/share this link/i)).toBeVisible(); -}); -``` +Place new specs in `frontend/e2e/`. Prefer role/label/`data-testid` selectors over CSS classes — see [E2E Guidelines](docs/testing/E2E_GUIDELINES.md). ```ts -// e2e/claim-flow.spec.ts +// e2e/home.spec.ts import { test, expect } from '@playwright/test'; -test('recipient can claim funds with a valid token', async ({ page }) => { - await page.goto('/claim/abc123mock'); - await expect(page.getByRole('heading', { name: /claim/i })).toBeVisible(); - await page.getByRole('button', { name: /claim funds/i }).click(); - await expect(page.getByText(/success/i)).toBeVisible(); +test('loads the homepage', async ({ page }) => { + await page.goto('/'); + await expect( + page.getByRole('heading', { name: /bridgelet payment flows/i }), + ).toBeVisible(); }); ``` -#### Running Playwright tests - -```bash -# Headless (CI-style) -npx playwright test - -# Interactive UI mode -npx playwright test --ui - -# Single file -npx playwright test e2e/send-flow.spec.ts - -# Debug mode (pauses on each step) -npx playwright test --debug -``` - -Add to `frontend/package.json`: - -```json -"scripts": { - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui" -} -``` +Deeper send/claim journeys that depend on wallets or the API should stay deterministic (mocked routes or sandbox pages) so CI does not hit live Stellar. #### CI integration -The existing `frontend-ci.yml` workflow will pick up `test:e2e` once it is added to the `test` script, or add a dedicated job: - -```yaml -- name: Install Playwright browsers - run: npx playwright install --with-deps chromium - working-directory: frontend - -- name: Run E2E tests - run: npm run test:e2e - working-directory: frontend -``` +`.github/workflows/frontend-ci.yml` builds the app, installs Chromium, then runs `npm run test:e2e` with `CI=true` so Playwright serves the production build via `next start`. On failure, the Playwright HTML report is uploaded as an artifact. --- @@ -761,16 +704,20 @@ E2E tests require a complete environment: ### Running E2E Tests ```bash -# Start the backend in test mode -npm run start:test +cd frontend -# In another terminal, run E2E tests +# One-time: install Chromium for Playwright +npx playwright install --with-deps chromium + +# Run the smoke suite (starts Next.js automatically) npm run test:e2e -# Run specific E2E test -npm run test:e2e -- claim-flow.spec.ts +# Run a specific file +npm run test:e2e -- e2e/home.spec.ts ``` +For full-stack journeys that need the SDK backend and Stellar testnet, start those services separately and keep browser specs deterministic (mocks/sandbox routes). See [Playwright E2E Tests](#playwright-e2e-tests) and [docs/testing/E2E_GUIDELINES.md](docs/testing/E2E_GUIDELINES.md). + ## Testing Against Live Testnet ### Prerequisites diff --git a/docs/testing/E2E_GUIDELINES.md b/docs/testing/E2E_GUIDELINES.md index 2b14387..2bf4362 100644 --- a/docs/testing/E2E_GUIDELINES.md +++ b/docs/testing/E2E_GUIDELINES.md @@ -2,6 +2,31 @@ Our frontend functional execution flows are guarded via automated Playwright UI verification suites to intercept interface breaking changes. +## Setup + +```bash +cd frontend +npm install +npx playwright install --with-deps chromium +``` + +Config lives at `frontend/playwright.config.ts` and targets **`http://localhost:3000`**. Specs live in `frontend/e2e/`. + +## Running + +```bash +cd frontend +npm run test:e2e # headless +npm run test:e2e:ui # interactive +``` + +- **Local:** Playwright starts `npm run dev` (or reuses a server already on `:3000`). +- **CI:** After `npm run build`, Playwright starts `npm run start` so E2E runs against the **built** Next.js app. + +See [TESTING.md](../../TESTING.md#playwright-e2e-tests) for the full guide. + ## Core Best Practices -* **Test Attribute Selectors:** Use explicit `data-testid` properties inside JSX code structures rather than styling classes to prevent UI refactoring breaks. -* **Deterministic Isolation:** Ensure testing targets interact against stable staging servers or mock environment routes (`/claim/test-token`) to prevent external ledger dependencies from destabilizing CI test performance. \ No newline at end of file + +* **Test Attribute Selectors:** Prefer role/label queries and explicit `data-testid` properties over styling classes to prevent UI refactoring breaks. +* **Deterministic Isolation:** Interact against stable pages or mock/sandbox routes (e.g. `/claim/example-token`, `/sandbox/*`) so external ledger dependencies do not destabilize CI. +* **Smoke first:** Keep CI green with navigation and page-render smoke tests; expand wallet/API journeys behind mocks. diff --git a/frontend/README.md b/frontend/README.md index 0f8c21c..3f8b4a8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -45,6 +45,19 @@ Reference Next.js UI for initiating and claiming crypto payments. npm run typecheck ``` +- Unit tests: + + ```bash + npm test + ``` + +- E2E tests (Playwright → `http://localhost:3000`): + + ```bash + npx playwright install chromium # one-time + npm run test:e2e + ``` + - Production build: ```bash diff --git a/frontend/e2e/home.spec.ts b/frontend/e2e/home.spec.ts new file mode 100644 index 0000000..9a6d3b1 --- /dev/null +++ b/frontend/e2e/home.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from '@playwright/test'; + +test.describe('homepage', () => { + test('loads the Bridgelet payment flows page', async ({ page }) => { + await page.goto('/'); + + await expect( + page.getByRole('heading', { name: /bridgelet payment flows/i }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /open sender flow/i }), + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /open claim flow/i }), + ).toBeVisible(); + }); + + test('navigates to the sender flow', async ({ page }) => { + await page.goto('/'); + await page.getByRole('link', { name: /open sender flow/i }).click(); + + await expect(page).toHaveURL(/\/send$/); + await expect( + page.getByRole('heading', { name: /send a payment/i }), + ).toBeVisible(); + }); + + test('navigates to the claim flow', async ({ page }) => { + await page.goto('/'); + await page.getByRole('link', { name: /open claim flow/i }).click(); + + await expect(page).toHaveURL(/\/claim\/example-token/); + await expect( + page.getByRole('heading', { name: /claim your payment/i }), + ).toBeVisible(); + }); +}); diff --git a/frontend/e2e/send.spec.ts b/frontend/e2e/send.spec.ts new file mode 100644 index 0000000..b2fcd25 --- /dev/null +++ b/frontend/e2e/send.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test'; + +test.describe('sender flow', () => { + test('renders the send page', async ({ page }) => { + await page.goto('/send'); + + await expect( + page.getByRole('heading', { name: /send a payment/i }), + ).toBeVisible(); + await expect( + page.getByText(/send crypto to anyone/i), + ).toBeVisible(); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 69b549c..c539ccf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "resend": "^6.16.0" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@storybook/nextjs-vite": "^10.4.6", "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -2634,6 +2635,22 @@ ], "peer": true }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@redocly/ajv": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", @@ -6796,6 +6813,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "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/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5266367..e577c4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,9 @@ "format:check": "prettier --check .", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@stellar/freighter-api": "^6.0.1", @@ -23,6 +25,7 @@ "resend": "^6.16.0" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@storybook/nextjs-vite": "^10.4.6", "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..d429b8d --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test'; + +const isCI = !!process.env.CI; + +/** + * Playwright E2E config for the Bridgelet reference UI. + * Targets http://localhost:3000. In CI the production server + * (`next start`) is used after `npm run build`; locally `next dev` + * is used unless a server is already running. + */ +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + reporter: isCI ? [['github'], ['list']] : 'list', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: isCI ? 'npm run start' : 'npm run dev', + url: 'http://localhost:3000', + reuseExistingServer: !isCI, + timeout: 120_000, + }, +});