test(e2e): replace Cypress with Playwright Electron E2E suite - #875
test(e2e): replace Cypress with Playwright Electron E2E suite#875raul-oliveira wants to merge 2 commits into
Conversation
Drive the real Electron desktop app end-to-end with Playwright, replacing the Cypress browser-based suite. Built on the four-layer architecture from rpc-lib PR #169: an Electron app driver, an Electron-blind page object, provisioning journeys, and specs. Journeys (green against public nodes): - onboarding: create a new wallet, reach the dashboard on the mainnet connect-default, assert a zero HTR total. - import: import a funded testnet seed (from E2E_IMPORT_SEED / .env.e2e), load on mainnet, then switch to testnet as an explicit spec verb and assert re-sync renders the transaction history. Changes: - Remove Cypress: delete cypress/, cypress.config.ts, the cypress devDeps and ESLint override; regenerate lavamoat policy. - Add tests/e2e/ harness: playwright.e2e.config.ts, worker-scoped Electron fixture (fresh --user-data-dir per project), page object, journeys, centralized env-tunable timeouts, hybrid selectors, .env.e2e loader, wallets registry, and e2e.md conventions. - Add a single data-testid="wallet-balance-total" to the dashboard Total balance (history reuses the existing #token-history table). - Replace the Cypress CI job with a Playwright job running under xvfb, with E2E_IMPORT_SEED provided via secret (import journey skips when unset).
📝 WalkthroughWalkthroughThis PR replaces the Cypress E2E testing stack with a Playwright + Electron setup. It removes Cypress config/tests/support files, updates CI workflow, gitignore/eslintignore, package scripts/dependencies, and LavaMoat policy allowlists. It adds Playwright config, env/build support, Electron driver, wallet UI helpers, journeys, fixtures, onboarding/import specs, documentation, and a test id on WalletBalance. ChangesCypress to Playwright E2E Migration
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Spec as Test Spec
participant Fixture as electron-fixture
participant Journeys as journeys.ts
participant WalletApp as WalletApp
participant Electron as electronApp driver
Spec->>Fixture: request wallet fixture
Fixture->>Electron: launchWallet()
Electron-->>Fixture: LaunchedApp (app, page)
Fixture->>WalletApp: new WalletApp(page)
Fixture->>Journeys: provisionNewWallet / provisionImportedWallet
Journeys->>WalletApp: chooseSoftwareWallet, createNewWords/enterSeed, setPassword, setPin
WalletApp-->>Journeys: expectDashboardLoaded
Fixture-->>Spec: provisioned WalletApp
Spec->>Journeys: switchToTestnet (import spec)
Journeys->>WalletApp: selectNetwork, enterNetworkPin, confirmTestnetModal
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add an E2E_TARGET=build mode so the same specs run against the LavaMoat-hardened production build (build/index.html via file://), not just the dev server. A Playwright globalSetup runs `npm run build`; the webServer is skipped and Electron launches without ELECTRON_START_URL so it loads the production bundle. Both journeys pass under LavaMoat's SES lockdown. - npm run e2e -> dev server (no LavaMoat) - npm run e2e:release -> production build with LavaMoat - E2E_SKIP_BUILD=1 reuses an existing build/ for faster iteration
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
.github/workflows/e2e.yml (1)
16-49: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider adding job-level
timeout-minutesand least-privilegepermissions.The job has no timeout, so a hung Electron/Playwright process (e.g. a stalled
xvfb-run -a npm run e2e) could run until the default GitHub Actions limit. Also nopermissions:block is set, so the job defaults to broader token scope than needed (this workflow only checks out code and runs tests).🛡️ Suggested additions
jobs: playwright-run: runs-on: ubuntu-22.04 + timeout-minutes: 30 + permissions: + contents: read strategy:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/e2e.yml around lines 16 - 49, The playwright-run job in the e2e workflow is missing guardrails: add a job-level timeout to prevent a hung xvfb-run / npm run e2e from running indefinitely, and add an explicit least-privilege permissions block since the job only needs to check out code and execute tests. Update the job definition itself so the timeout and permissions apply to all steps, keeping the existing Checkout, Setup nodejs, and Playwright test steps unchanged.tests/e2e/import.spec.ts (1)
15-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid duplicating the PIN literal; use the shared default.
switchToTestnetalready defaultspintoCREDENTIALS.pin(the same value used byprovisionImportedWalletto set up the wallet). Passing'123456'explicitly duplicates that value outside its single source of truth — ifCREDENTIALS.pinever changes, this call silently uses a stale PIN and the testnet switch will fail.♻️ Proposed fix
- await switchToTestnet(wallet, '123456'); + await switchToTestnet(wallet);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/e2e/import.spec.ts` around lines 15 - 21, The test is duplicating the wallet PIN by passing a hardcoded value into switchToTestnet even though that helper already defaults to CREDENTIALS.pin. Update the call in the test that switches the Hathor network to testnet so it relies on switchToTestnet’s default PIN instead of supplying a literal, keeping it aligned with provisionImportedWallet and the shared credential source.tests/e2e/helpers/walletApp.ts (1)
90-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffDirect hash navigation bypasses real UI flows.
openNetworkSettings,expectConnectedToTestnet, andgoToDashboardnavigate by settingwindow.location.hashviapage.evaluaterather than clicking through the app's own nav/menu. This is a reasonable pragmatic shortcut for E2E setup, but it means these paths never exercise the actual navigation UI (menu links, routing guards). Worth keeping in mind if a future regression in the nav UI itself goes undetected by this suite.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/e2e/helpers/walletApp.ts` around lines 90 - 139, The navigation helpers in walletApp use direct hash assignment via page.evaluate in openNetworkSettings, expectConnectedToTestnet, and goToDashboard, which bypasses the app’s real navigation UI. Update these helpers to drive navigation through the actual visible menu/link controls or a shared UI navigation helper so the E2E flow exercises routing guards and menu behavior instead of only the hash-based route changes.playwright.e2e.config.ts (1)
46-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInline env-var shell syntax breaks on Windows.
PORT=${devServerPort} BROWSER=none npm startrelies on POSIX shellVAR=value cmdsyntax, which fails under Windowscmd.exe/PowerShell. Playwright'swebServer.envoption handles this portably.♻️ Proposed fix
? { - command: `PORT=${devServerPort} BROWSER=none npm start`, + command: 'npm start', + env: { PORT: devServerPort, BROWSER: 'none' }, url: devServerUrl,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playwright.e2e.config.ts` around lines 46 - 56, The webServer command in playwright.e2e.config.ts uses POSIX inline env assignment, which breaks on Windows. Update the webServer configuration for the target === 'dev' branch to pass PORT and BROWSER through the webServer env option instead of prefixing them in the command string, and keep the existing command focused on starting the app with npm start.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/e2e.yml:
- Around line 24-25: The Checkout step in the e2e workflow should disable git
credential persistence. Update the actions/checkout usage in the Checkout job to
set persist-credentials to false so the job does not retain the token after
cloning. Keep the change scoped to the checkout step only, since no later steps
in this workflow need to push or reuse git credentials.
In `@tests/e2e/driver/electronApp.ts`:
- Around line 24-53: The temporary userDataDir created in launchWallet can leak
when electron.launch() or app.firstWindow() throws before closeWallet ever runs.
Update launchWallet to ensure cleanup happens on all failure paths, ideally by
wrapping the launch and firstWindow flow in try/finally and removing the temp
directory if startup fails, while preserving the returned app/page/userDataDir
shape on success. Use the launchWallet helper and the userDataDir variable as
the main points to locate and adjust the lifecycle handling.
---
Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 16-49: The playwright-run job in the e2e workflow is missing
guardrails: add a job-level timeout to prevent a hung xvfb-run / npm run e2e
from running indefinitely, and add an explicit least-privilege permissions block
since the job only needs to check out code and execute tests. Update the job
definition itself so the timeout and permissions apply to all steps, keeping the
existing Checkout, Setup nodejs, and Playwright test steps unchanged.
In `@playwright.e2e.config.ts`:
- Around line 46-56: The webServer command in playwright.e2e.config.ts uses
POSIX inline env assignment, which breaks on Windows. Update the webServer
configuration for the target === 'dev' branch to pass PORT and BROWSER through
the webServer env option instead of prefixing them in the command string, and
keep the existing command focused on starting the app with npm start.
In `@tests/e2e/helpers/walletApp.ts`:
- Around line 90-139: The navigation helpers in walletApp use direct hash
assignment via page.evaluate in openNetworkSettings, expectConnectedToTestnet,
and goToDashboard, which bypasses the app’s real navigation UI. Update these
helpers to drive navigation through the actual visible menu/link controls or a
shared UI navigation helper so the E2E flow exercises routing guards and menu
behavior instead of only the hash-based route changes.
In `@tests/e2e/import.spec.ts`:
- Around line 15-21: The test is duplicating the wallet PIN by passing a
hardcoded value into switchToTestnet even though that helper already defaults to
CREDENTIALS.pin. Update the call in the test that switches the Hathor network to
testnet so it relies on switchToTestnet’s default PIN instead of supplying a
literal, keeping it aligned with provisionImportedWallet and the shared
credential source.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b75e5993-27ec-4120-9000-29b16fea0dc7
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (26)
.env.e2e.example.eslintignore.github/workflows/e2e.yml.gitignorecypress.config.tscypress/e2e/00-welcome.cy.jscypress/support/commands.jscypress/support/e2e.jslavamoat/webpack/policy.jsonpackage.jsonplaywright.e2e.config.tssrc/components/WalletBalance.jstests/e2e/driver/electronApp.tstests/e2e/driver/timeouts.tstests/e2e/e2e.mdtests/e2e/fixtures/electron-fixture.tstests/e2e/helpers/journeys.tstests/e2e/helpers/selectors.tstests/e2e/helpers/walletApp.tstests/e2e/helpers/wallets.tstests/e2e/import.spec.tstests/e2e/onboarding.spec.tstests/e2e/support/build.tstests/e2e/support/env.tstests/e2e/tsconfig.jsontests/e2e/wallets.config.json
💤 Files with no reviewable changes (4)
- cypress/support/commands.js
- cypress/e2e/00-welcome.cy.js
- cypress/support/e2e.js
- cypress.config.ts
| - name: Checkout | ||
| # https://github.com/actions/checkout/releases/tag/v4.2.2 | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Set persist-credentials: false on checkout.
Static analysis flags that the checkout step doesn't disable credential persistence. Since this job doesn't need to push or use git credentials afterward, disable persistence to reduce token exposure surface.
🔒 Suggested fix
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Checkout | |
| # https://github.com/actions/checkout/releases/tag/v4.2.2 | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 24-25: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/e2e.yml around lines 24 - 25, The Checkout step in the e2e
workflow should disable git credential persistence. Update the actions/checkout
usage in the Checkout job to set persist-credentials to false so the job does
not retain the token after cloning. Keep the change scoped to the checkout step
only, since no later steps in this workflow need to push or reuse git
credentials.
Source: Linters/SAST tools
| export async function launchWallet(): Promise<LaunchedApp> { | ||
| const userDataDir = mkdtempSync(join(tmpdir(), 'hathor-wallet-e2e-')); | ||
|
|
||
| // build target: no ELECTRON_START_URL -> public/electron.js loads the production | ||
| // build/index.html via file:// (LavaMoat applied). dev target: point Electron at | ||
| // the CRA dev server (no LavaMoat). | ||
| const target = process.env.E2E_TARGET === 'build' ? 'build' : 'dev'; | ||
| const env: NodeJS.ProcessEnv = { ...process.env, SENTRY_DSN: DUMMY_SENTRY_DSN }; | ||
| if (target === 'build') { | ||
| delete env.ELECTRON_START_URL; | ||
| env.NODE_ENV = 'production'; | ||
| } else { | ||
| env.ELECTRON_START_URL = env.ELECTRON_START_URL ?? 'http://localhost:3000'; | ||
| env.NODE_ENV = 'dev'; | ||
| } | ||
|
|
||
| const app = await electron.launch({ | ||
| executablePath: electronBinary, | ||
| cwd: repoRoot, | ||
| args: ['.', '--no-sandbox', `--user-data-dir=${userDataDir}`], | ||
| // electron.launch types env as { [k: string]: string }; Node's process.env is | ||
| // { [k: string]: string | undefined }. At runtime present keys are strings, so | ||
| // reconcile the two here rather than filtering undefined values away. | ||
| env: env as { [key: string]: string }, | ||
| timeout: TIMEOUTS.appLaunch, | ||
| }); | ||
| const page = await app.firstWindow(); | ||
| await page.waitForLoadState('domcontentloaded'); | ||
| return { app, page, userDataDir }; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Temp userDataDir leaks if electron.launch() or firstWindow() throws.
mkdtempSync runs before the launch; if electron.launch times out/fails or app.firstWindow() rejects, the temp directory is never removed (closeWallet is never reached since launchWallet never returns an app to close). Over repeated CI failures this accumulates stray temp dirs.
🔧 Proposed fix
export async function launchWallet(): Promise<LaunchedApp> {
const userDataDir = mkdtempSync(join(tmpdir(), 'hathor-wallet-e2e-'));
-
- // build target: ...
- const target = process.env.E2E_TARGET === 'build' ? 'build' : 'dev';
- const env: NodeJS.ProcessEnv = { ...process.env, SENTRY_DSN: DUMMY_SENTRY_DSN };
- if (target === 'build') {
- delete env.ELECTRON_START_URL;
- env.NODE_ENV = 'production';
- } else {
- env.ELECTRON_START_URL = env.ELECTRON_START_URL ?? 'http://localhost:3000';
- env.NODE_ENV = 'dev';
- }
-
- const app = await electron.launch({
- executablePath: electronBinary,
- cwd: repoRoot,
- args: ['.', '--no-sandbox', `--user-data-dir=${userDataDir}`],
- env: env as { [key: string]: string },
- timeout: TIMEOUTS.appLaunch,
- });
- const page = await app.firstWindow();
- await page.waitForLoadState('domcontentloaded');
- return { app, page, userDataDir };
+ try {
+ const target = process.env.E2E_TARGET === 'build' ? 'build' : 'dev';
+ const env: NodeJS.ProcessEnv = { ...process.env, SENTRY_DSN: DUMMY_SENTRY_DSN };
+ if (target === 'build') {
+ delete env.ELECTRON_START_URL;
+ env.NODE_ENV = 'production';
+ } else {
+ env.ELECTRON_START_URL = env.ELECTRON_START_URL ?? 'http://localhost:3000';
+ env.NODE_ENV = 'dev';
+ }
+
+ const app = await electron.launch({
+ executablePath: electronBinary,
+ cwd: repoRoot,
+ args: ['.', '--no-sandbox', `--user-data-dir=${userDataDir}`],
+ env: env as { [key: string]: string },
+ timeout: TIMEOUTS.appLaunch,
+ });
+ const page = await app.firstWindow();
+ await page.waitForLoadState('domcontentloaded');
+ return { app, page, userDataDir };
+ } catch (err) {
+ rmSync(userDataDir, { recursive: true, force: true });
+ throw err;
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function launchWallet(): Promise<LaunchedApp> { | |
| const userDataDir = mkdtempSync(join(tmpdir(), 'hathor-wallet-e2e-')); | |
| // build target: no ELECTRON_START_URL -> public/electron.js loads the production | |
| // build/index.html via file:// (LavaMoat applied). dev target: point Electron at | |
| // the CRA dev server (no LavaMoat). | |
| const target = process.env.E2E_TARGET === 'build' ? 'build' : 'dev'; | |
| const env: NodeJS.ProcessEnv = { ...process.env, SENTRY_DSN: DUMMY_SENTRY_DSN }; | |
| if (target === 'build') { | |
| delete env.ELECTRON_START_URL; | |
| env.NODE_ENV = 'production'; | |
| } else { | |
| env.ELECTRON_START_URL = env.ELECTRON_START_URL ?? 'http://localhost:3000'; | |
| env.NODE_ENV = 'dev'; | |
| } | |
| const app = await electron.launch({ | |
| executablePath: electronBinary, | |
| cwd: repoRoot, | |
| args: ['.', '--no-sandbox', `--user-data-dir=${userDataDir}`], | |
| // electron.launch types env as { [k: string]: string }; Node's process.env is | |
| // { [k: string]: string | undefined }. At runtime present keys are strings, so | |
| // reconcile the two here rather than filtering undefined values away. | |
| env: env as { [key: string]: string }, | |
| timeout: TIMEOUTS.appLaunch, | |
| }); | |
| const page = await app.firstWindow(); | |
| await page.waitForLoadState('domcontentloaded'); | |
| return { app, page, userDataDir }; | |
| } | |
| export async function launchWallet(): Promise<LaunchedApp> { | |
| const userDataDir = mkdtempSync(join(tmpdir(), 'hathor-wallet-e2e-')); | |
| try { | |
| // build target: no ELECTRON_START_URL -> public/electron.js loads the production | |
| // build/index.html via file:// (LavaMoat applied). dev target: point Electron at | |
| // the CRA dev server (no LavaMoat). | |
| const target = process.env.E2E_TARGET === 'build' ? 'build' : 'dev'; | |
| const env: NodeJS.ProcessEnv = { ...process.env, SENTRY_DSN: DUMMY_SENTRY_DSN }; | |
| if (target === 'build') { | |
| delete env.ELECTRON_START_URL; | |
| env.NODE_ENV = 'production'; | |
| } else { | |
| env.ELECTRON_START_URL = env.ELECTRON_START_URL ?? 'http://localhost:3000'; | |
| env.NODE_ENV = 'dev'; | |
| } | |
| const app = await electron.launch({ | |
| executablePath: electronBinary, | |
| cwd: repoRoot, | |
| args: ['.', '--no-sandbox', `--user-data-dir=${userDataDir}`], | |
| // electron.launch types env as { [k: string]: string }; Node's process.env is | |
| // { [k: string]: string | undefined }. At runtime present keys are strings, so | |
| // reconcile the two here rather than filtering undefined values away. | |
| env: env as { [key: string]: string }, | |
| timeout: TIMEOUTS.appLaunch, | |
| }); | |
| const page = await app.firstWindow(); | |
| await page.waitForLoadState('domcontentloaded'); | |
| return { app, page, userDataDir }; | |
| } catch (err) { | |
| rmSync(userDataDir, { recursive: true, force: true }); | |
| throw err; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/e2e/driver/electronApp.ts` around lines 24 - 53, The temporary
userDataDir created in launchWallet can leak when electron.launch() or
app.firstWindow() throws before closeWallet ever runs. Update launchWallet to
ensure cleanup happens on all failure paths, ideally by wrapping the launch and
firstWindow flow in try/finally and removing the temp directory if startup
fails, while preserving the returned app/page/userDataDir shape on success. Use
the launchWallet helper and the userDataDir variable as the main points to
locate and adjust the lifecycle handling.
Summary
Replaces the Cypress browser-based E2E suite with Playwright driving the real Electron desktop app. Built on the four-layer architecture established for the rpc-lib web-wallet in HathorNetwork/hathor-rpc-lib#169 (Electron app driver / Electron-blind page object / provisioning journeys / specs), and reused for the explorer in HathorNetwork/hathor-explorer#519.
This is PR #1 of a series: it lays the harness + conventions and ships the two onboarding journeys. Funded sends and token-lifecycle are deferred to follow-ups.
How it works: Playwright's
webServerboots the CRA dev server; a worker-scoped fixture launches the installed Electron 27 binary via_electron.launchwith a fresh--user-data-dirper project (= one isolated wallet). The testnet switch is an explicit spec verb (per the #169 doctrine). Selectors are hybrid (roles/text/ids), isolated in oneselectors.ts; timeouts are centralized and env-tunable (E2E_TIMEOUT_SCALE).Test evidence (real runs against public nodes):
onboarding→1 passed— new wallet → dashboard →Total: 0.00 HTRon mainnet.import→2 passed— funded seed → mainnet dashboard → switch to testnet → re-sync → transaction history rendered.E2E_IMPORT_SEEDunset, the import journey is skipped.Run locally:
cp .env.e2e.example .env.e2e(setE2E_IMPORT_SEEDto a funded testnet stub seed), thennpm run e2e(ornpm run e2e -- --project=onboarding,npm run e2e:headed).Acceptance Criteria
cypress/,cypress.config.ts, the three Cypress devDependencies, and the Cypress ESLint override; lavamoat policy regenerated without Cypress entries.tests/e2e/: config, worker-scoped Electron fixture (fresh--user-data-dirper project), Electron app driver, Electron-blind page object, provisioning journeys, centralized env-tunable timeouts, hybrid selectors, dependency-free.env.e2eloader, wallets registry, and a read-firste2e.mdconventions doc.e2e/e2e:headed/e2e:uinpm scripts added;.env.e2e,playwright-report/,test-results/,blob-report/gitignored.Total: 0.00 HTR.E2E_IMPORT_SEED) → mainnet dashboard → switch network to testnet as an explicit spec verb → assert re-sync renders the transaction history. Skipped when the seed env var is unset.data-testid="wallet-balance-total"on the dashboard Total balance (history reuses the existing#token-historytable — no extra testid).xvfb-run, withE2E_IMPORT_SEEDprovided via a repository secret (import journey skips green until the secret is set).Security Checklist
@playwright/test(a devDependency, test-only); three Cypress devDependencies were removed. No production dependencies were added or changed. The funded import seed is a public testnet stub kept out of the repo — it is loaded from a gitignored.env.e2elocally and a CI secret in the workflow; the import journey skips when it is unset.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores