Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7e3c6a2
test(smoke): setup playwright test suite
vinzenzLIFI Apr 2, 2026
59746c5
test(ci): verify examples builds on gha
vinzenzLIFI Apr 2, 2026
b26f5ff
test(ci): fix Playwright not found in CI due to isolated e2e workspace
vinzenzLIFI Apr 2, 2026
af7c510
fix(example): camelcase import for privy builds not recognised on ci
vinzenzLIFI Apr 2, 2026
e681f3f
fix(e2e-ci): wrong privy ethers project name
vinzenzLIFI Apr 2, 2026
59c3c8a
fix(e2e-ci): diff not detected when package name instead of directory…
vinzenzLIFI Apr 2, 2026
68eb88b
docs(tests): workspace inclusion not reflected in readme
vinzenzLIFI Apr 2, 2026
d87aa12
fix: duplicate node and ts dependencies for e2e tests
vinzenzLIFI Apr 15, 2026
d6d0ab2
test(playground): isolate playground suite from examples
vinzenzLIFI May 6, 2026
c4f48e7
test(examples): add examples test suite with profile-based projects
vinzenzLIFI May 6, 2026
8a6a522
test(examples): add local build/serve/test scripts
vinzenzLIFI May 6, 2026
e5d652d
ci(examples): replace smoke workflow with path-filtered matrix
vinzenzLIFI May 6, 2026
3cfd1cf
docs(e2e): document playground + examples suites
vinzenzLIFI May 6, 2026
c0f0821
chore(deps): sync pnpm-lock with prior dep cleanup
vinzenzLIFI May 6, 2026
5dcd3d8
fix(e2e): increase assertion timeout and add webServer for playground…
vinzenzLIFI May 6, 2026
82b53b4
test: deliberately break vite and connectkit to verify failure comment
vinzenzLIFI May 7, 2026
9efdf07
fix(ci): upgrade upload-artifact to v7 and fix reporter override
vinzenzLIFI May 7, 2026
1b8152f
chore(e2e): reduce expect timeout to 10s
vinzenzLIFI May 11, 2026
a582357
docs(e2e): fix stale spec filename and document failure comment
vinzenzLIFI May 7, 2026
63932f9
ci(e2e): always update sticky comment to reflect current run result
vinzenzLIFI May 7, 2026
e4591c9
ci(e2e): cancel in-progress runs on new push
vinzenzLIFI May 7, 2026
3890c96
refactor(e2e): consolidate example list into a single JSON source of …
vinzenzLIFI May 19, 2026
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
194 changes: 194 additions & 0 deletions .github/workflows/e2e-examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: E2E Examples

on:
pull_request:
types: [opened, reopened, synchronize]
branches: [main]

concurrency:
group: e2e-examples-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read

jobs:
# ── Detect which examples need testing ──────────────────────────────────────
detect-changes:
name: Detect affected examples
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
matrix: ${{ steps.build-matrix.outputs.matrix }}
has-examples: ${{ steps.build-matrix.outputs.has-examples }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Build test matrix
id: build-matrix
run: |
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}

CHANGED=$(git diff --name-only "$BASE_SHA"..."$HEAD_SHA")

mapfile -t ALL_EXAMPLES < <(jq -r '.[] | select(.status=="active") | .name' e2e/examples.json)

# Check if shared packages changed — if so, run all examples
SHARED_CHANGED=false
while IFS= read -r file; do
case "$file" in
packages/widget/*|packages/widget-provider*/**|packages/wallet-management/*|e2e/*)
SHARED_CHANGED=true ;;
pnpm-workspace.yaml|pnpm-lock.yaml|package.json)
SHARED_CHANGED=true ;;
esac
done <<< "$CHANGED"

AFFECTED=()
if [ "$SHARED_CHANGED" = "true" ]; then
AFFECTED=("${ALL_EXAMPLES[@]}")
else
for name in "${ALL_EXAMPLES[@]}"; do
if echo "$CHANGED" | grep -q "^examples/${name}/"; then
AFFECTED+=("$name")
fi
done
fi

if [ ${#AFFECTED[@]} -eq 0 ]; then
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has-examples=false" >> "$GITHUB_OUTPUT"
echo "No affected examples detected."
else
JSON=$(printf '%s\n' "${AFFECTED[@]}" | jq -R . | jq -sc .)
echo "matrix=$JSON" >> "$GITHUB_OUTPUT"
echo "has-examples=true" >> "$GITHUB_OUTPUT"
echo "Affected examples: $JSON"
fi

# ── Run tests per example ────────────────────────────────────────────────────
test:
name: E2E ${{ matrix.example }}
needs: detect-changes
if: needs.detect-changes.outputs.has-examples == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
example: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install dependencies
uses: ./.github/actions/pnpm-install

- name: Build workspace packages
run: pnpm -r --parallel --filter './packages/**' --filter !'*-playground-*' --filter !'*-embedded' build

- name: Resolve example metadata
id: meta
run: |
NAME="${{ matrix.example }}"
Q='.[] | select(.name==$n)'
echo "pkg=$(jq -r --arg n "$NAME" "$Q | .pkg" e2e/examples.json)" >> "$GITHUB_OUTPUT"
echo "build=$(jq -r --arg n "$NAME" "$Q | .buildCmd" e2e/examples.json)" >> "$GITHUB_OUTPUT"
echo "serve=$(jq -r --arg n "$NAME" "$Q | .serveCmd" e2e/examples.json)" >> "$GITHUB_OUTPUT"
echo "port=$(jq -r --arg n "$NAME" "$Q | .port" e2e/examples.json)" >> "$GITHUB_OUTPUT"
echo "serve_env=$(jq -r --arg n "$NAME" "$Q | .serveEnv // {} | to_entries | map(.key+\"=\"+.value) | join(\" \")" e2e/examples.json)" >> "$GITHUB_OUTPUT"

- name: Build example
run: |
if [ "${{ steps.meta.outputs.build }}" = "vite-build" ]; then
pnpm --filter "${{ steps.meta.outputs.pkg }}" exec vite build
else
pnpm --filter "${{ steps.meta.outputs.pkg }}" build
fi

- name: Start example server
run: |
SERVE_ENV="${{ steps.meta.outputs.serve_env }}"
if [ -n "$SERVE_ENV" ]; then
env $SERVE_ENV pnpm --filter "${{ steps.meta.outputs.pkg }}" "${{ steps.meta.outputs.serve }}" &
else
pnpm --filter "${{ steps.meta.outputs.pkg }}" "${{ steps.meta.outputs.serve }}" &
fi

URL="http://localhost:${{ steps.meta.outputs.port }}"
for i in $(seq 1 60); do
if curl -sf "$URL" > /dev/null 2>&1; then
echo "Server ready at $URL after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::Server failed to start within 60s at $URL"
exit 1

- name: Install Playwright browsers
run: pnpm --filter @lifi/widget-e2e exec playwright install chromium --with-deps

- name: Run example tests
run: |
pnpm --filter @lifi/widget-e2e exec playwright test \
--config playwright.examples.config.ts \
--project "${{ matrix.example }}"

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-${{ matrix.example }}
path: e2e/playwright-report-examples/
retention-days: 7

# ── Summarise failures as a single PR comment ────────────────────────────────
report:
name: Report results
needs: [detect-changes, test]
if: always() && needs.detect-changes.outputs.has-examples == 'true' && needs.test.result != 'skipped'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Find failed examples
if: needs.test.result == 'failure'
id: failures
env:
GH_TOKEN: ${{ github.token }}
run: |
FAILED=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \
--paginate \
--jq '[.jobs[] | select(.name | test("^E2E ")) | select(.conclusion == "failure") | .name | ltrimstr("E2E ")] | sort | map("- `"+.+"`") | join("\n")')
echo "list<<EOF" >> "$GITHUB_OUTPUT"
echo "$FAILED" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"

- name: Post success comment
if: needs.test.result == 'success'
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: e2e-results
message: |
## E2E Examples — all passed

All examples passed in the [latest run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).

- name: Post failure comment
if: needs.test.result == 'failure'
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: e2e-results
message: |
## E2E Examples — failures

The following example(s) failed:

${{ steps.failures.outputs.list }}

See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for Playwright reports and logs.
7 changes: 7 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
test-results/
playwright-report/
playwright-report-examples/
.auth/
.env.test
*.local
180 changes: 180 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# LI.FI Widget — E2E Test Suite

Playwright TypeScript E2E tests for the LI.FI Widget. Two test suites share this directory:

| Suite | Config | What it tests |
|---|---|---|
| **Playground** | `playwright.config.ts` | Widget playground (`packages/widget-playground-vite`) |
| **Examples** | `playwright.examples.config.ts` | All example apps in `examples/` |

---

## Setup

Dependencies are installed automatically with `pnpm install` from the repo root.

```bash
# Install Playwright browsers (once, or after Playwright version bumps)
pnpm --filter @lifi/widget-e2e exec playwright install chromium
```

### Prerequisite: build workspace packages

Examples consume `@lifi/widget`, `@lifi/wallet-management`, etc. as workspace deps that resolve to `packages/*/dist`. Stale dist artifacts cause silent runtime failures (e.g. `Class extends value undefined …` when the widget mounts), so build packages before running the example suite:

```bash
# From repo root
pnpm -r --parallel --filter './packages/**' --filter '!*-playground-*' --filter '!*-embedded' build
```

CI runs the same command before tests.

---

## Playground Tests

The playground must be running before tests can execute.

```bash
# Terminal 1
pnpm dev # starts widget-playground-vite on http://localhost:3000

# Terminal 2 — from repo root
pnpm smoketest # playground smoke tests
```

From the `e2e/` directory:

| Command | Description |
|---|---|
| `pnpm test` | Full test suite |
| `pnpm smoketest` | Smoke tests only |
| `pnpm test:headed` | Visible browser |
| `pnpm test:debug` | Playwright debug inspector |
| `pnpm test:ui` | Playwright interactive UI |
| `pnpm report` | Open last HTML report |

---

## Example Tests

Each example is built, served, and tested in isolation. The local scripts handle the full lifecycle.

```bash
# From repo root

# Single example — build → serve → test → kill
pnpm test:example vite
pnpm test:example tanstack-router
pnpm test:example nft-checkout

# All 17 active examples sequentially
pnpm test:examples
```

Reports land in `e2e/playwright-report-examples/`.

### Active examples and their profiles

| Profile | Examples | What makes it different |
|---|---|---|
| `standard` | vite, connectkit, privy, privy-ethers, rainbowkit, reown, svelte, zustand-widget-config, vue, nextjs, nextjs15, remix, react-router-7 | Widget at `/`, Exchange heading |
| `routed` | tanstack-router | Widget at a custom route (`/widget`) |
| `iframe` | vite-iframe, vite-iframe-wagmi | Widget inside `<iframe>` via `LiFiWidgetLight`; loads from `https://widget.li.fi` |
| `nft` | nft-checkout | NFT checkout subvariant — Checkout heading, Pay with section |

### What each profile asserts

**standard / routed** — widget root visible, Exchange heading, From/To buttons, send amount input, Settings view opens with all rows, back navigation, token selector From/To end-to-end.

**iframe** — iframe present in DOM, widget root visible inside frame, Exchange heading inside frame, Settings button clickable inside frame.

**nft** — widget root visible, Checkout heading, Pay with section, no error boundary.

### Per-example build/serve notes

Most examples use the framework's standard preview command. A few quirks captured in `e2e/examples.config.ts`:

- **`buildCmd: 'vite-build'`** (instead of `'build'`) — used for examples whose `package.json` build script is `tsc && vite build`, where `tsc` currently fails due to a MUI v7 props vs. `@lifi/types` mismatch. We invoke Vite directly to skip the type check until upstream types catch up. Affects: `vite`, `connectkit`, `privy`, `privy-ethers`, `rainbowkit`, `reown`, `vite-iframe`, `dynamic`, `deposit-flow`.
- **`PORT` env var** — `remix` and `react-router-7` use `remix-serve` / `react-router-serve`, which honor `process.env.PORT` and otherwise pick the next free port. The script always sets `PORT` for these.
- **`nextjs` / `nextjs15`** — `next start` defaults to port 3000.
- **`nuxt`** — `nuxt preview` defaults to port 3000.

### Known broken examples (not tested)

> Fix these and flip `status: 'broken' → 'active'` in `e2e/examples.config.ts` to include them.

| Example | Issue | Ticket |
|---|---|---|
| `dynamic` | `vite-plugin-env-compatible` doesn't shim `process` globally — Dynamic SDK crashes at runtime | [EMB-349](https://linear.app/lifi-linear/issue/EMB-349) |
| `nuxt` | veaury's React bridge fails in Nuxt SSR production build (`R is not a function`) | [EMB-350](https://linear.app/lifi-linear/issue/EMB-350) |
| `deposit-flow` | Widget's own error boundary fires at runtime — root never mounts | [EMB-351](https://linear.app/lifi-linear/issue/EMB-351) |

### Stale directories (ignore)

`examples/nextjs14`, `examples/nextjs14-page-router`, and `examples/nextjs-page-router` have no `package.json` and are leftover scaffolding. They are not in `examples.config.ts` and are not built or tested.

### Adding a new example

Add a single entry to `e2e/examples.json` — it is the only source of truth. The Playwright config, CI workflow, and local scripts all read from it automatically.

---

## Architecture

### Two configs, clean separation

`playwright.config.ts` runs the playground suite (`tests/playground/`). It ignores `tests/profiles/` entirely via `testIgnore`.

`playwright.examples.config.ts` generates one Playwright project per active example from `examples.config.ts`. Each project sets `baseURL` to the example's port and `testMatch` to its profile spec. No `webServer` — server lifecycle is managed externally by the scripts or CI.

### Component Object Model

The widget uses internal TanStack Router navigation that does not change the URL. A Page Object Model with URL-based boundaries doesn't apply. Each widget view has its own Component Object:

```
tests/
├── fixtures/
│ └── base.fixture.ts # Extended test with widget fixtures + waitForTokens()
├── components/
│ ├── PlaygroundSidebar.ts # Left sidebar: Design/Code tabs, variant controls
│ ├── WidgetExchange.ts # Exchange view: From/To buttons, Settings icon, send input
│ ├── TokenSelectorView.ts # Token list, chain sidebar
│ └── SettingsView.ts # Settings rows, back navigation
├── playground/
│ └── smoke.spec.ts # Playground smoke tests
└── profiles/
├── widget-smoke.spec.ts # standard + routed (reads mountPath from project metadata)
├── iframe.spec.ts
└── nft.spec.ts
```

### Selector strategy

| Use | Avoid |
|---|---|
| `getByRole('button', { name: '...' })` | CSS class names — MUI generates dynamic names |
| `locator('[id^="widget-app-expanded-container"]')` for widget root | `locator('main')` — playground-specific, absent in example apps |
| `locator('p', { hasText: /^Exchange$/ })` for headings | `getByRole('paragraph')` — `<p>` has no implicit ARIA role |
| `getByRole('list').locator('listitem')` for token rows | Positional index assumptions without prior search |

**Widget root:** `<div id="widget-app-expanded-container-{suffix}">` — the suffix varies per build session, always use the prefix selector `[id^="widget-app-expanded-container"]`.

### Token list

The widget fetches tokens on page load, not on selector open. Use `waitForTokens(page)` paired with `page.goto()` in a `Promise.all` to guarantee the response is captured:

```ts
await Promise.all([waitForTokens(page), page.goto('/')])
```

---

## CI

`e2e-examples.yml` triggers on every PR to `main`. The `detect-changes` job computes which examples to test:

- A change to `examples/<name>/` → runs that example only
- A change to `packages/widget/**`, `packages/wallet-management/**`, `packages/widget-provider*/**`, or `e2e/**` → runs all 17 examples

Each example runs as an isolated matrix job (parallel, `fail-fast: false`). If any example fails, a sticky comment is posted on the PR listing which ones failed with a link to the workflow run.
Loading
Loading