Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,38 +29,52 @@ 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");
const pkg = require("./package.json");
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
139 changes: 43 additions & 96 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions docs/testing/E2E_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

* **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.
13 changes: 13 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions frontend/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from '@playwright/test';

test.describe('homepage', () => {

Check failure on line 3 in frontend/e2e/home.spec.ts

View workflow job for this annotation

GitHub Actions / Lint, type-check, test, and build

e2e/home.spec.ts

Error: Playwright Test did not expect test.describe() to be called here. Most common reasons include: - You are calling test.describe() in a configuration file. - You are calling test.describe() in a file that is imported by the configuration file. - You have two different versions of @playwright/test. This usually happens when one of the dependencies in your package.json depends on @playwright/test. - You are calling test.describe() from an async test.describe() block. Only sync ones are supported. ❯ _TestTypeImpl._currentSuite node_modules/playwright/lib/common/index.js:2257:13 ❯ _TestTypeImpl._describe node_modules/playwright/lib/common/index.js:2298:24 ❯ Function.describe node_modules/playwright/lib/common/index.js:1220:12 ❯ e2e/home.spec.ts:3:6
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();
});
});
14 changes: 14 additions & 0 deletions frontend/e2e/send.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { test, expect } from '@playwright/test';

test.describe('sender flow', () => {

Check failure on line 3 in frontend/e2e/send.spec.ts

View workflow job for this annotation

GitHub Actions / Lint, type-check, test, and build

e2e/send.spec.ts

Error: Playwright Test did not expect test.describe() to be called here. Most common reasons include: - You are calling test.describe() in a configuration file. - You are calling test.describe() in a file that is imported by the configuration file. - You have two different versions of @playwright/test. This usually happens when one of the dependencies in your package.json depends on @playwright/test. - You are calling test.describe() from an async test.describe() block. Only sync ones are supported. ❯ _TestTypeImpl._currentSuite node_modules/playwright/lib/common/index.js:2257:13 ❯ _TestTypeImpl._describe node_modules/playwright/lib/common/index.js:2298:24 ❯ Function.describe node_modules/playwright/lib/common/index.js:1220:12 ❯ e2e/send.spec.ts:3:6
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();
});
});
Loading
Loading