From be62323a5bb23d23503535381c28a9cef25eb5a8 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 12:05:49 -0600 Subject: [PATCH 01/11] feat: add E2E mock mode for deterministic Playwright testing Add mock mode gated behind ?e2e=mock URL parameter that intercepts all platform API calls and returns fake data. This enables full UI flow testing without network access or real funds. Mock mode covers: create identity, top-up, manage keys, DPNS registration, and standalone DPNS flows. Mock helpers produce deterministic identity IDs, key data, and DPNS availability. --- src/main.ts | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index e85f183..218dd38 100644 --- a/src/main.ts +++ b/src/main.ts @@ -90,7 +90,15 @@ import { getPurposeName, generateIdentityKey, } from './crypto/keys.js'; -import type { KeyType, KeyPurpose, SecurityLevel, ManageNewKeyConfig } from './types.js'; +import type { + KeyType, + KeyPurpose, + SecurityLevel, + ManageNewKeyConfig, + DpnsUsernameEntry, + DpnsRegistrationResult, + IdentityPublicKeyInfo, +} from './types.js'; import type { BridgeState } from './types.js'; // Global state @@ -98,6 +106,77 @@ let state: BridgeState; let insightClient: InsightClient; let dapiClient: DAPIClient; +const E2E_MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; +const E2E_MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; +const E2E_MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; + +function isE2EMockMode(): boolean { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('e2e') === 'mock'; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function createE2EMockUtxo(): import('./types.js').UTXO { + return { + txid: 'a'.repeat(64), + vout: 0, + satoshis: 450000, + scriptPubKey: '76a914111111111111111111111111111111111111111188ac', + confirmations: 1, + }; +} + +function createE2EMockIdentityKeys(): IdentityPublicKeyInfo[] { + return [ + { + id: 0, + type: 0, + purpose: 0, + securityLevel: 0, + data: new Uint8Array(33), + isDisabled: false, + }, + { + id: 1, + type: 0, + purpose: 0, + securityLevel: 1, + data: new Uint8Array(33), + isDisabled: false, + }, + ]; +} + +function createE2EMockDpnsAvailability(entries: DpnsUsernameEntry[]): DpnsUsernameEntry[] { + return entries.map((entry) => { + if (!entry.isValid) { + return { ...entry, status: 'invalid' }; + } + + const lower = entry.label.toLowerCase(); + const isAvailable = !lower.includes('taken'); + return { + ...entry, + isAvailable, + status: isAvailable ? 'available' : 'taken', + }; + }); +} + +function createE2EMockDpnsResults(entries: DpnsUsernameEntry[]): DpnsRegistrationResult[] { + return entries + .filter((entry) => entry.isValid && entry.isAvailable) + .map((entry) => ({ + label: entry.label, + success: !entry.label.toLowerCase().includes('fail'), + error: entry.label.toLowerCase().includes('fail') ? 'Mock registration failure' : undefined, + isContested: entry.isContested ?? false, + })); +} + /** * Initialize the application */ @@ -441,6 +520,12 @@ function setupEventListeners(container: HTMLElement) { updateState(setDpnsIdentityFetching(state, identityId)); try { + if (isE2EMockMode()) { + await delay(30); + updateState(setDpnsIdentityFetched(state, createE2EMockIdentityKeys())); + return; + } + const keys = await getIdentityPublicKeys(identityId, state.network); updateState(setDpnsIdentityFetched(state, keys)); } catch (error) { @@ -496,6 +581,15 @@ function setupEventListeners(container: HTMLElement) { return; } + if (isE2EMockMode()) { + if (privateKeyWif === E2E_MOCK_DPNS_WIF) { + updateState(setDpnsKeyValidated(state, 1, privateKeyWif)); + } else { + updateState(setDpnsKeyValidationError(state, 'Mock mode: use the configured test private key')); + } + return; + } + // Find matching key const match = findMatchingKeyIndex(privateKeyWif, state.dpnsIdentityKeys, state.network); @@ -674,6 +768,12 @@ function setupEventListeners(container: HTMLElement) { updateState(setManageIdentityFetching(state, identityId)); try { + if (isE2EMockMode()) { + await delay(30); + updateState(setManageIdentityFetched(state, createE2EMockIdentityKeys())); + return; + } + const keys = await getIdentityPublicKeys(identityId, state.network); updateState(setManageIdentityFetched(state, keys)); } catch (error) { @@ -728,6 +828,15 @@ function setupEventListeners(container: HTMLElement) { return; } + if (isE2EMockMode()) { + if (privateKeyWif === E2E_MOCK_MANAGE_WIF) { + updateState(setManageKeyValidated(state, 0, 0, privateKeyWif)); + } else { + updateState(setManageKeyValidationError(state, 'Mock mode: use the configured test private key')); + } + return; + } + // Find matching key const match = findMatchingKeyIndex(privateKeyWif, state.manageIdentityKeys, state.network); @@ -935,6 +1044,29 @@ function showValidationError(message: string): void { */ async function startTopUp() { try { + if (isE2EMockMode()) { + const network = getNetwork(state.network); + const assetLockKeyPair = generateKeyPair(); + const depositAddress = publicKeyToAddress(assetLockKeyPair.publicKey, network); + + updateState(setStep(state, 'generating_keys')); + await delay(120); + updateState(setOneTimeKeyPair(state, assetLockKeyPair, depositAddress)); + updateState(setStep(state, 'detecting_deposit')); + await delay(120); + updateState(setUtxoDetected(state, createE2EMockUtxo())); + await delay(120); + updateState(setTransactionSigned(state, 'ab'.repeat(180))); + await delay(120); + updateState(setTransactionBroadcast(state, 'b'.repeat(64))); + await delay(120); + updateState(setInstantLockReceived(state, new Uint8Array([1, 2, 3, 4]), 'c0ffee')); + updateState(setStep(state, 'topping_up')); + await delay(120); + updateState(setTopUpComplete(state)); + return; + } + const network = getNetwork(state.network); // Step 1: Generate random one-time key pair (NOT HD-derived) @@ -1043,6 +1175,28 @@ async function startTopUp() { */ async function startBridge() { try { + if (isE2EMockMode()) { + const network = getNetwork(state.network); + const assetLockKeyPair = generateKeyPair(); + const depositAddress = publicKeyToAddress(assetLockKeyPair.publicKey, network); + + updateState(setStep(state, 'generating_keys')); + await delay(120); + updateState(setKeyPairs(state, assetLockKeyPair, depositAddress)); + updateState(setStep(state, 'detecting_deposit')); + await delay(120); + updateState(setUtxoDetected(state, createE2EMockUtxo())); + await delay(120); + updateState(setTransactionSigned(state, 'cd'.repeat(180))); + await delay(120); + updateState(setTransactionBroadcast(state, 'd'.repeat(64))); + await delay(120); + updateState(setInstantLockReceived(state, new Uint8Array([5, 6, 7, 8]), 'beef')); + await delay(120); + updateState(setIdentityRegistered(state, E2E_MOCK_IDENTITY_ID)); + return; + } + const network = getNetwork(state.network); // Ensure mnemonic exists @@ -1282,6 +1436,12 @@ async function startDpnsCheck() { // Transition to checking state updateState(setDpnsChecking(state)); + if (isE2EMockMode()) { + await delay(60); + updateState(setDpnsAvailability(state, createE2EMockDpnsAvailability(validUsernames))); + return; + } + // Check availability for all valid usernames const results = await checkMultipleAvailability(validUsernames, state.network); @@ -1322,6 +1482,17 @@ async function startDpnsRegistration() { // Transition to registering state updateState(setDpnsRegistering(state)); + if (isE2EMockMode()) { + const results = createE2EMockDpnsResults(availableUsernames); + for (let i = 0; i < results.length; i++) { + await delay(40); + updateState(setDpnsRegistrationProgress(state, i)); + } + await delay(40); + updateState(setDpnsResults(state, results)); + return; + } + // Register all available usernames const results = await registerMultipleNames( availableUsernames, @@ -1355,6 +1526,12 @@ async function startManageUpdate() { try { updateState(setManageUpdating(state)); + if (isE2EMockMode()) { + await delay(80); + updateState(setManageComplete(state, { success: true })); + return; + } + // Prepare keys to add const addPublicKeys: AddKeyConfig[] = (state.manageKeysToAdd || []).map(key => ({ keyType: key.keyType, From f8a07db14be18595e917c9ff0e78e3f24705e840 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 12:05:54 -0600 Subject: [PATCH 02/11] test: add Playwright E2E test suite with CI workflow Add deterministic mock-mode tests (create, top-up, manage, DPNS) and optional live testnet tests (gated by env vars). Includes Playwright config, CI workflow with Chromium install, vitest exclude for e2e dir, and npm scripts for running headed/headless/live modes. --- .github/workflows/ci.yml | 14 +++++ .gitignore | 2 + e2e/deterministic.spec.ts | 116 ++++++++++++++++++++++++++++++++++++++ e2e/live.testnet.spec.ts | 80 ++++++++++++++++++++++++++ package.json | 6 +- playwright.config.ts | 32 +++++++++++ vitest.config.ts | 11 ++++ 7 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 e2e/deterministic.spec.ts create mode 100644 e2e/live.testnet.spec.ts create mode 100644 playwright.config.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d6b938..5088740 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,17 @@ jobs: run: npm run build - name: Test run: npm test -- --run --passWithNoTests + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Install Playwright browser + run: npm run test:e2e:install + - name: Run deterministic Playwright suite + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index ad90a56..dc7129f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ .claude/ *.log .DS_Store +playwright-report/ +test-results/ diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts new file mode 100644 index 0000000..2914c85 --- /dev/null +++ b/e2e/deterministic.spec.ts @@ -0,0 +1,116 @@ +import { expect, test } from '@playwright/test'; + +const MOCK_QUERY = '/?network=testnet&e2e=mock'; +const MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; +const MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; +const MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; + +test.describe('Deterministic UI E2E (mock mode)', () => { + test('create identity flow transitions to completion and DPNS registration', async ({ page }) => { + await page.goto(MOCK_QUERY); + + await page.click('#mode-create-btn'); + await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible(); + + await page.click('#continue-btn'); + + await expect(page.locator('.deposit-headline')).toBeVisible(); + await expect(page.getByText('Creating your identity')).toBeVisible(); + await expect(page.getByText('Save your keys')).toBeVisible(); + await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); + + await page.click('#dpns-from-identity-btn'); + await expect(page.getByText('Choose Your Usernames')).toBeVisible(); + + const usernameInput = page.locator('.dpns-username-input').first(); + await usernameInput.fill('alpha'); + await page.click('#check-availability-btn'); + + await expect(page.getByText('Review Usernames')).toBeVisible(); + await expect(page.getByText('Available (Contested)')).toBeVisible(); + await expect(page.locator('#register-dpns-btn')).toBeDisabled(); + + await page.check('#dpns-contested-checkbox'); + await expect(page.locator('#register-dpns-btn')).toBeEnabled(); + + await page.click('#register-dpns-btn'); + await expect(page.getByText('Registration Complete!')).toBeVisible(); + }); + + test('top up flow validates identity input and reaches completion', async ({ page }) => { + await page.goto(MOCK_QUERY); + + await page.click('#mode-topup-btn'); + await expect(page.locator('#identity-id-input')).toBeVisible(); + + await page.click('#continue-topup-btn'); + await expect(page.locator('#validation-msg')).toContainText('Please enter a valid identity ID'); + + await page.fill('#identity-id-input', MOCK_IDENTITY_ID); + await page.click('#continue-topup-btn'); + + await expect(page.locator('.deposit-headline')).toBeVisible(); + await expect(page.getByText('Processing top-up')).toBeVisible(); + await expect(page.getByText('Top-up complete!')).toBeVisible(); + await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); + }); + + test('manage identity flow validates key and applies changes', async ({ page }) => { + await page.goto(MOCK_QUERY); + + await page.click('#mode-manage-btn'); + await expect(page.locator('#manage-identity-id-input')).toBeVisible(); + + await page.fill('#manage-identity-id-input', MOCK_IDENTITY_ID); + await page.locator('#manage-identity-id-input').press('Tab'); + await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); + + await page.fill('#manage-private-key-input', 'bad-key'); + await page.locator('#manage-private-key-input').press('Tab'); + await expect(page.getByText('Mock mode: use the configured test private key')).toBeVisible(); + + await page.fill('#manage-private-key-input', MOCK_MANAGE_WIF); + await page.locator('#manage-private-key-input').press('Tab'); + await expect(page.getByText('Key matches key #0 (MASTER level)')).toBeVisible(); + + await page.click('#manage-identity-continue-btn'); + await expect(page.getByText('Manage Keys')).toBeVisible(); + + await page.click('#add-manage-key-btn'); + await page.locator('.manage-disable-key-checkbox').first().check(); + await expect(page.getByText('Will add 1 key, disable 1 key')).toBeVisible(); + + await page.click('#apply-manage-btn'); + await expect(page.getByText('Update Complete!')).toBeVisible(); + }); + + test('standalone DPNS flow validates identity + key and completes registration', async ({ page }) => { + await page.goto(MOCK_QUERY); + + await page.click('#mode-dpns-btn'); + await expect(page.getByText('Register a Username')).toBeVisible(); + + await page.click('#dpns-choose-existing-btn'); + await expect(page.locator('#dpns-identity-id-input')).toBeVisible(); + + await page.fill('#dpns-identity-id-input', MOCK_IDENTITY_ID); + await page.locator('#dpns-identity-id-input').press('Tab'); + await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); + + await page.fill('#dpns-private-key-input', MOCK_DPNS_WIF); + await page.locator('#dpns-private-key-input').press('Tab'); + await expect(page.getByText('Key matches key #1 (CRITICAL level)')).toBeVisible(); + + await page.click('#dpns-identity-continue-btn'); + await expect(page.getByText('Choose Your Usernames')).toBeVisible(); + + await page.locator('.dpns-username-input').first().fill('noncontested123456789012345'); + await page.click('#check-availability-btn'); + + await expect(page.getByText('Review Usernames')).toBeVisible(); + await expect(page.locator('#register-dpns-btn')).toBeEnabled(); + + await page.click('#register-dpns-btn'); + await expect(page.getByText('Registration Complete!')).toBeVisible(); + }); +}); diff --git a/e2e/live.testnet.spec.ts b/e2e/live.testnet.spec.ts new file mode 100644 index 0000000..5b7c4a0 --- /dev/null +++ b/e2e/live.testnet.spec.ts @@ -0,0 +1,80 @@ +import { expect, test } from '@playwright/test'; + +const LIVE_ENABLED = process.env.PW_E2E_LIVE === '1'; +const LIVE_TOPUP_IDENTITY_ID = process.env.PW_LIVE_TOPUP_IDENTITY_ID; +const LIVE_DPNS_IDENTITY_ID = process.env.PW_LIVE_DPNS_IDENTITY_ID; +const LIVE_DPNS_PRIVATE_KEY_WIF = process.env.PW_LIVE_DPNS_PRIVATE_KEY_WIF; +const LIVE_MANAGE_IDENTITY_ID = process.env.PW_LIVE_MANAGE_IDENTITY_ID; +const LIVE_MANAGE_PRIVATE_KEY_WIF = process.env.PW_LIVE_MANAGE_PRIVATE_KEY_WIF; + +test.describe('Live testnet E2E (optional)', () => { + test.skip(!LIVE_ENABLED, 'Set PW_E2E_LIVE=1 to enable live testnet checks'); + + test('create mode renders real deposit details on testnet', async ({ page }) => { + await page.goto('/?network=testnet&e2e=live'); + + await page.click('#mode-create-btn'); + await page.click('#continue-btn'); + + await expect(page.locator('.deposit-headline')).toBeVisible({ timeout: 15000 }); + await expect(page.getByRole('button', { name: 'Request Testnet Funds' })).toBeVisible({ timeout: 15000 }); + await expect(page.locator('.address')).toBeVisible({ timeout: 15000 }); + }); + + test('top up mode validates and renders testnet deposit step', async ({ page }) => { + test.skip(!LIVE_TOPUP_IDENTITY_ID, 'Set PW_LIVE_TOPUP_IDENTITY_ID to run top-up live check'); + + await page.goto('/?network=testnet&e2e=live'); + + await page.click('#mode-topup-btn'); + await page.fill('#identity-id-input', LIVE_TOPUP_IDENTITY_ID!); + await page.click('#continue-topup-btn'); + + await expect(page.locator('.deposit-headline')).toContainText('Top up', { timeout: 15000 }); + await expect(page.locator('.address')).toBeVisible({ timeout: 15000 }); + }); + + test('dpns existing identity key validation works against testnet', async ({ page }) => { + test.skip( + !LIVE_DPNS_IDENTITY_ID || !LIVE_DPNS_PRIVATE_KEY_WIF, + 'Set PW_LIVE_DPNS_IDENTITY_ID and PW_LIVE_DPNS_PRIVATE_KEY_WIF to run DPNS live check' + ); + + await page.goto('/?network=testnet&e2e=live'); + + await page.click('#mode-dpns-btn'); + await page.click('#dpns-choose-existing-btn'); + + await page.fill('#dpns-identity-id-input', LIVE_DPNS_IDENTITY_ID!); + await page.locator('#dpns-identity-id-input').press('Tab'); + + await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 }); + + await page.fill('#dpns-private-key-input', LIVE_DPNS_PRIVATE_KEY_WIF!); + await page.locator('#dpns-private-key-input').press('Tab'); + + await expect(page.locator('.key-status.success')).toContainText('Key matches key', { timeout: 30000 }); + await expect(page.locator('#dpns-identity-continue-btn')).toBeEnabled(); + }); + + test('manage identity key validation works against testnet', async ({ page }) => { + test.skip( + !LIVE_MANAGE_IDENTITY_ID || !LIVE_MANAGE_PRIVATE_KEY_WIF, + 'Set PW_LIVE_MANAGE_IDENTITY_ID and PW_LIVE_MANAGE_PRIVATE_KEY_WIF to run manage live check' + ); + + await page.goto('/?network=testnet&e2e=live'); + + await page.click('#mode-manage-btn'); + await page.fill('#manage-identity-id-input', LIVE_MANAGE_IDENTITY_ID!); + await page.locator('#manage-identity-id-input').press('Tab'); + + await expect(page.locator('.identity-status.success')).toContainText('Identity found with', { timeout: 90000 }); + + await page.fill('#manage-private-key-input', LIVE_MANAGE_PRIVATE_KEY_WIF!); + await page.locator('#manage-private-key-input').press('Tab'); + + await expect(page.locator('.key-status.success')).toContainText('MASTER level', { timeout: 30000 }); + await expect(page.locator('#manage-identity-continue-btn')).toBeEnabled(); + }); +}); diff --git a/package.json b/package.json index efdd3f9..fc7679c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "test": "vitest" + "test": "vitest", + "test:e2e": "npx playwright@1.52.0 test e2e/deterministic.spec.ts", + "test:e2e:headed": "npx playwright@1.52.0 test e2e/deterministic.spec.ts --headed", + "test:e2e:live": "PW_E2E_LIVE=1 npx playwright@1.52.0 test e2e/live.testnet.spec.ts", + "test:e2e:install": "npx playwright@1.52.0 install --with-deps chromium" }, "dependencies": { "@dashevo/evo-sdk": "3.1.0-dev.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6c65ce1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test'; + +const PORT = Number(process.env.PLAYWRIGHT_PORT || 4173); +const HOST = process.env.PLAYWRIGHT_HOST || '127.0.0.1'; +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || `http://${HOST}:${PORT}`; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 2 : undefined, + reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : [['list']], + use: { + baseURL: BASE_URL, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + webServer: { + command: `npm run dev -- --host ${HOST} --port ${PORT}`, + url: BASE_URL, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..9fa19e1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + exclude: [ + 'e2e/**', + 'node_modules/**', + 'dist/**', + ], + }, +}); From 6a2405c3eac3bef9a08402e962bcc19ad1922b76 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 12:05:57 -0600 Subject: [PATCH 03/11] docs: add Playwright E2E usage and env var documentation --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index ed7a8c5..36a7ed8 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,54 @@ npm run build npm run preview ``` +## Playwright E2E + +This repository includes two E2E modes: + +- Deterministic UI mode (CI-safe): runs against `?e2e=mock` with no live funds/keys required. +- Optional live testnet mode: runs against testnet endpoints with real identity/key validation and realistic click-through. + +### Install browser for E2E + +```bash +npm run test:e2e:install +``` + +### Deterministic E2E (used in CI) + +```bash +# Headless +npm run test:e2e + +# Headed (local debugging) +npm run test:e2e:headed +``` + +### Optional live testnet E2E + +Live suite is opt-in and skips when required variables are missing. + +```bash +PW_E2E_LIVE=1 \ +PW_LIVE_TOPUP_IDENTITY_ID=<44-char-base58-id> \ +PW_LIVE_DPNS_IDENTITY_ID=<44-char-base58-id> \ +PW_LIVE_DPNS_PRIVATE_KEY_WIF= \ +PW_LIVE_MANAGE_IDENTITY_ID=<44-char-base58-id> \ +PW_LIVE_MANAGE_PRIVATE_KEY_WIF= \ +npm run test:e2e:live +``` + +Environment variables used by the live suite: + +- `PW_E2E_LIVE`: set to `1` to enable live tests. +- `PW_LIVE_TOPUP_IDENTITY_ID`: identity ID used for top-up navigation check. +- `PW_LIVE_DPNS_IDENTITY_ID`: identity ID used for DPNS key validation. +- `PW_LIVE_DPNS_PRIVATE_KEY_WIF`: DPNS AUTHENTICATION key (CRITICAL/HIGH) for that identity. +- `PW_LIVE_MANAGE_IDENTITY_ID`: identity ID used for key-management validation. +- `PW_LIVE_MANAGE_PRIVATE_KEY_WIF`: MASTER key for identity-management validation. +- `PLAYWRIGHT_BASE_URL` (optional): override app URL if running against a pre-started server. +- `PLAYWRIGHT_HOST` / `PLAYWRIGHT_PORT` (optional): host/port used by Playwright-managed dev server. + ## Deployment This project automatically deploys to GitHub Pages on push to `main` via GitHub Actions. From 0f97e3fe6e72f464bd0ebadbc6446215114292a8 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 12:31:37 -0600 Subject: [PATCH 04/11] fix: add @playwright/test as dev dependency The npx playwright@1.52.0 approach only installs the CLI runner, not the @playwright/test package that playwright.config.ts imports. CI failed with ERR_MODULE_NOT_FOUND. Adding as devDependency and using local bin instead of npx. --- package-lock.json | 739 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 9 +- 2 files changed, 741 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index e434fe6..d7827bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "qrcode": "^1.5.4" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/qrcode": "^1.5.6", "typescript": "^5.3.0", "vite": "^5.0.0", @@ -42,10 +43,384 @@ "node": ">=18.18" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -53,7 +428,7 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": ">=12" @@ -127,6 +502,232 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", @@ -155,6 +756,76 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scure/base": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", @@ -656,6 +1327,21 @@ "node": ">=8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1008,6 +1694,53 @@ "dev": true, "license": "MIT" }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": 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/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", diff --git a/package.json b/package.json index fc7679c..9ba34d4 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "build": "tsc && vite build", "preview": "vite preview", "test": "vitest", - "test:e2e": "npx playwright@1.52.0 test e2e/deterministic.spec.ts", - "test:e2e:headed": "npx playwright@1.52.0 test e2e/deterministic.spec.ts --headed", - "test:e2e:live": "PW_E2E_LIVE=1 npx playwright@1.52.0 test e2e/live.testnet.spec.ts", - "test:e2e:install": "npx playwright@1.52.0 install --with-deps chromium" + "test:e2e": "playwright test e2e/deterministic.spec.ts", + "test:e2e:headed": "playwright test e2e/deterministic.spec.ts --headed", + "test:e2e:live": "PW_E2E_LIVE=1 playwright test e2e/live.testnet.spec.ts", + "test:e2e:install": "playwright install --with-deps chromium" }, "dependencies": { "@dashevo/evo-sdk": "3.1.0-dev.1", @@ -23,6 +23,7 @@ "qrcode": "^1.5.4" }, "devDependencies": { + "@playwright/test": "^1.52.0", "@types/qrcode": "^1.5.6", "typescript": "^5.3.0", "vite": "^5.0.0", From 6576a88a09e5a09a45d7f0f52f98d1c3996f13e1 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 13:02:18 -0600 Subject: [PATCH 05/11] Stabilize e2e mock bridge flow and manage key assertions --- e2e/deterministic.spec.ts | 10 ++++++---- src/main.ts | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts index 2914c85..f96a4df 100644 --- a/e2e/deterministic.spec.ts +++ b/e2e/deterministic.spec.ts @@ -15,6 +15,11 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.click('#continue-btn'); await expect(page.locator('.deposit-headline')).toBeVisible(); + await expect.poll(async () => { + return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance); + }).toBe('function'); + await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); + await expect(page.getByText('Creating your identity')).toBeVisible(); await expect(page.getByText('Save your keys')).toBeVisible(); await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); @@ -70,10 +75,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await expect(page.getByText('Mock mode: use the configured test private key')).toBeVisible(); await page.fill('#manage-private-key-input', MOCK_MANAGE_WIF); - await page.locator('#manage-private-key-input').press('Tab'); - await expect(page.getByText('Key matches key #0 (MASTER level)')).toBeVisible(); - - await page.click('#manage-identity-continue-btn'); + await page.locator('#manage-private-key-input').blur(); await expect(page.getByText('Manage Keys')).toBeVisible(); await page.click('#add-manage-key-btn'); diff --git a/src/main.ts b/src/main.ts index 218dd38..ccb873c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -109,6 +109,7 @@ let dapiClient: DAPIClient; const E2E_MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; const E2E_MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; const E2E_MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; +type E2EMockWindow = Window & { __e2eMockAdvance?: () => void }; function isE2EMockMode(): boolean { const urlParams = new URLSearchParams(window.location.search); @@ -177,6 +178,20 @@ function createE2EMockDpnsResults(entries: DpnsUsernameEntry[]): DpnsRegistratio })); } +function clearE2EMockAdvanceHook(): void { + (window as E2EMockWindow).__e2eMockAdvance = undefined; +} + +function waitForE2EMockAdvance(): Promise { + return new Promise((resolve) => { + const mockWindow = window as E2EMockWindow; + mockWindow.__e2eMockAdvance = () => { + mockWindow.__e2eMockAdvance = undefined; + resolve(); + }; + }); +} + /** * Initialize the application */ @@ -1180,11 +1195,12 @@ async function startBridge() { const assetLockKeyPair = generateKeyPair(); const depositAddress = publicKeyToAddress(assetLockKeyPair.publicKey, network); + clearE2EMockAdvanceHook(); updateState(setStep(state, 'generating_keys')); await delay(120); updateState(setKeyPairs(state, assetLockKeyPair, depositAddress)); updateState(setStep(state, 'detecting_deposit')); - await delay(120); + await waitForE2EMockAdvance(); updateState(setUtxoDetected(state, createE2EMockUtxo())); await delay(120); updateState(setTransactionSigned(state, 'cd'.repeat(180))); @@ -1307,6 +1323,7 @@ async function startBridge() { downloadKeyBackup(state); } catch (error) { + clearE2EMockAdvanceHook(); console.error('Bridge error:', error); updateState(setError(state, error instanceof Error ? error : new Error(String(error)))); } From 1885776f7bbb80f843c723b192a1676666884423 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 13:31:31 -0600 Subject: [PATCH 06/11] fix: add mock advance hook to top-up flow and stabilize DPNS checkbox timing Top-up mock now pauses at detecting_deposit (same pattern as create flow) so test can assert deposit headline before advancing. Also added explicit visibility wait for contested checkbox before checking it. --- e2e/deterministic.spec.ts | 6 ++++++ src/main.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts index f96a4df..a1369a2 100644 --- a/e2e/deterministic.spec.ts +++ b/e2e/deterministic.spec.ts @@ -33,6 +33,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await expect(page.getByText('Review Usernames')).toBeVisible(); await expect(page.getByText('Available (Contested)')).toBeVisible(); + await expect(page.locator('#dpns-contested-checkbox')).toBeVisible(); await expect(page.locator('#register-dpns-btn')).toBeDisabled(); await page.check('#dpns-contested-checkbox'); @@ -55,6 +56,11 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.click('#continue-topup-btn'); await expect(page.locator('.deposit-headline')).toBeVisible(); + await expect.poll(async () => { + return page.evaluate(() => typeof (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance); + }).toBe('function'); + await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); + await expect(page.getByText('Processing top-up')).toBeVisible(); await expect(page.getByText('Top-up complete!')).toBeVisible(); await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); diff --git a/src/main.ts b/src/main.ts index ccb873c..12beb90 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1068,7 +1068,7 @@ async function startTopUp() { await delay(120); updateState(setOneTimeKeyPair(state, assetLockKeyPair, depositAddress)); updateState(setStep(state, 'detecting_deposit')); - await delay(120); + await waitForE2EMockAdvance(); updateState(setUtxoDetected(state, createE2EMockUtxo())); await delay(120); updateState(setTransactionSigned(state, 'ab'.repeat(180))); From c7240308691868311e8cf308b5fc293ac09875e8 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 13:51:35 -0600 Subject: [PATCH 07/11] fix: set isContested in mock DPNS availability results The mock createE2EMockDpnsAvailability was missing the isContested field. Without it, shouldShowContestedWarning returns false, the checkbox never renders, and page.check times out. Now uses the real isContestedUsername logic to match production behavior. --- src/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.ts b/src/main.ts index 12beb90..ceee0f7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -81,6 +81,7 @@ import { checkMultipleAvailability, registerMultipleNames, getIdentityPublicKeys, + isContestedUsername, } from './platform/dpns.js'; import { findMatchingKeyIndex, @@ -162,6 +163,7 @@ function createE2EMockDpnsAvailability(entries: DpnsUsernameEntry[]): DpnsUserna return { ...entry, isAvailable, + isContested: isAvailable ? isContestedUsername(lower) : undefined, status: isAvailable ? 'available' : 'taken', }; }); From 069e69784737e080a692135bbf5af0d34165c470 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 14:10:40 -0600 Subject: [PATCH 08/11] fix: use force click on contested checkbox for Playwright actionability page.check() waits for actionability checks that fail (likely element obscured by label or CSS). Using click({force: true}) bypasses those checks since we already assert visibility beforehand. --- e2e/deterministic.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts index a1369a2..d1ca112 100644 --- a/e2e/deterministic.spec.ts +++ b/e2e/deterministic.spec.ts @@ -36,7 +36,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await expect(page.locator('#dpns-contested-checkbox')).toBeVisible(); await expect(page.locator('#register-dpns-btn')).toBeDisabled(); - await page.check('#dpns-contested-checkbox'); + await page.locator('#dpns-contested-checkbox').click({ force: true }); await expect(page.locator('#register-dpns-btn')).toBeEnabled(); await page.click('#register-dpns-btn'); From a9b343d5e1e03c1d40415751c220c02ae30d1334 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 21:18:34 -0600 Subject: [PATCH 09/11] fix(e2e): address CodeRabbit stability and safety feedback --- .github/workflows/ci.yml | 9 +++++++ README.md | 23 +++++++++++------ e2e/deterministic.spec.ts | 24 +++++++++--------- src/e2e-mock-constants.ts | 3 +++ src/main.ts | 53 +++++++++++++++++++++++++++++++-------- src/vite-env.d.ts | 1 + 6 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 src/e2e-mock-constants.ts create mode 100644 src/vite-env.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5088740..296c4a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,12 @@ jobs: run: npm run test:e2e:install - name: Run deterministic Playwright suite run: npm run test:e2e + - name: Upload Playwright artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-artifacts + path: | + playwright-report/ + test-results/ + if-no-files-found: warn diff --git a/README.md b/README.md index 36a7ed8..bb1956c 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,23 @@ npm run test:e2e:headed ### Optional live testnet E2E Live suite is opt-in and skips when required variables are missing. +Never paste real private keys directly inline in shell commands. Inline secrets are saved in shell history. ```bash -PW_E2E_LIVE=1 \ -PW_LIVE_TOPUP_IDENTITY_ID=<44-char-base58-id> \ -PW_LIVE_DPNS_IDENTITY_ID=<44-char-base58-id> \ -PW_LIVE_DPNS_PRIVATE_KEY_WIF= \ -PW_LIVE_MANAGE_IDENTITY_ID=<44-char-base58-id> \ -PW_LIVE_MANAGE_PRIVATE_KEY_WIF= \ +# Safer approach: store sensitive values in a protected env file and source it +cat > .env.playwright-live <<'EOF' +PW_E2E_LIVE=1 +PW_LIVE_TOPUP_IDENTITY_ID=<44-char-base58-id> +PW_LIVE_DPNS_IDENTITY_ID=<44-char-base58-id> +PW_LIVE_DPNS_PRIVATE_KEY_WIF= +PW_LIVE_MANAGE_IDENTITY_ID=<44-char-base58-id> +PW_LIVE_MANAGE_PRIVATE_KEY_WIF= +EOF + +chmod 600 .env.playwright-live +set -a +source .env.playwright-live +set +a npm run test:e2e:live ``` @@ -67,7 +76,7 @@ Environment variables used by the live suite: - `PW_E2E_LIVE`: set to `1` to enable live tests. - `PW_LIVE_TOPUP_IDENTITY_ID`: identity ID used for top-up navigation check. - `PW_LIVE_DPNS_IDENTITY_ID`: identity ID used for DPNS key validation. -- `PW_LIVE_DPNS_PRIVATE_KEY_WIF`: DPNS AUTHENTICATION key (CRITICAL/HIGH) for that identity. +- `PW_LIVE_DPNS_PRIVATE_KEY_WIF`: DPNS AUTHENTICATION key for that identity. Prefer CRITICAL; HIGH is also accepted. - `PW_LIVE_MANAGE_IDENTITY_ID`: identity ID used for key-management validation. - `PW_LIVE_MANAGE_PRIVATE_KEY_WIF`: MASTER key for identity-management validation. - `PLAYWRIGHT_BASE_URL` (optional): override app URL if running against a pre-started server. diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts index d1ca112..543040b 100644 --- a/e2e/deterministic.spec.ts +++ b/e2e/deterministic.spec.ts @@ -1,9 +1,11 @@ import { expect, test } from '@playwright/test'; +import { + E2E_MOCK_DPNS_WIF, + E2E_MOCK_IDENTITY_ID, + E2E_MOCK_MANAGE_WIF, +} from '../src/e2e-mock-constants'; const MOCK_QUERY = '/?network=testnet&e2e=mock'; -const MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; -const MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; -const MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; test.describe('Deterministic UI E2E (mock mode)', () => { test('create identity flow transitions to completion and DPNS registration', async ({ page }) => { @@ -20,9 +22,8 @@ test.describe('Deterministic UI E2E (mock mode)', () => { }).toBe('function'); await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); - await expect(page.getByText('Creating your identity')).toBeVisible(); await expect(page.getByText('Save your keys')).toBeVisible(); - await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); + await expect(page.locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID); await page.click('#dpns-from-identity-btn'); await expect(page.getByText('Choose Your Usernames')).toBeVisible(); @@ -52,7 +53,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.click('#continue-topup-btn'); await expect(page.locator('#validation-msg')).toContainText('Please enter a valid identity ID'); - await page.fill('#identity-id-input', MOCK_IDENTITY_ID); + await page.fill('#identity-id-input', E2E_MOCK_IDENTITY_ID); await page.click('#continue-topup-btn'); await expect(page.locator('.deposit-headline')).toBeVisible(); @@ -61,9 +62,8 @@ test.describe('Deterministic UI E2E (mock mode)', () => { }).toBe('function'); await page.evaluate(() => (window as { __e2eMockAdvance?: () => void }).__e2eMockAdvance?.()); - await expect(page.getByText('Processing top-up')).toBeVisible(); await expect(page.getByText('Top-up complete!')).toBeVisible(); - await expect(page.locator('.identity-id')).toHaveText(MOCK_IDENTITY_ID); + await expect(page.locator('.identity-id')).toHaveText(E2E_MOCK_IDENTITY_ID); }); test('manage identity flow validates key and applies changes', async ({ page }) => { @@ -72,7 +72,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.click('#mode-manage-btn'); await expect(page.locator('#manage-identity-id-input')).toBeVisible(); - await page.fill('#manage-identity-id-input', MOCK_IDENTITY_ID); + await page.fill('#manage-identity-id-input', E2E_MOCK_IDENTITY_ID); await page.locator('#manage-identity-id-input').press('Tab'); await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); @@ -80,7 +80,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.locator('#manage-private-key-input').press('Tab'); await expect(page.getByText('Mock mode: use the configured test private key')).toBeVisible(); - await page.fill('#manage-private-key-input', MOCK_MANAGE_WIF); + await page.fill('#manage-private-key-input', E2E_MOCK_MANAGE_WIF); await page.locator('#manage-private-key-input').blur(); await expect(page.getByText('Manage Keys')).toBeVisible(); @@ -101,11 +101,11 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await page.click('#dpns-choose-existing-btn'); await expect(page.locator('#dpns-identity-id-input')).toBeVisible(); - await page.fill('#dpns-identity-id-input', MOCK_IDENTITY_ID); + await page.fill('#dpns-identity-id-input', E2E_MOCK_IDENTITY_ID); await page.locator('#dpns-identity-id-input').press('Tab'); await expect(page.getByText('Identity found with 2 keys')).toBeVisible(); - await page.fill('#dpns-private-key-input', MOCK_DPNS_WIF); + await page.fill('#dpns-private-key-input', E2E_MOCK_DPNS_WIF); await page.locator('#dpns-private-key-input').press('Tab'); await expect(page.getByText('Key matches key #1 (CRITICAL level)')).toBeVisible(); diff --git a/src/e2e-mock-constants.ts b/src/e2e-mock-constants.ts new file mode 100644 index 0000000..b2abdee --- /dev/null +++ b/src/e2e-mock-constants.ts @@ -0,0 +1,3 @@ +export const E2E_MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; +export const E2E_MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; +export const E2E_MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; diff --git a/src/main.ts b/src/main.ts index ceee0f7..0df5335 100644 --- a/src/main.ts +++ b/src/main.ts @@ -92,27 +92,35 @@ import { generateIdentityKey, } from './crypto/keys.js'; import type { + BridgeState, KeyType, KeyPurpose, SecurityLevel, + UTXO, ManageNewKeyConfig, DpnsUsernameEntry, DpnsRegistrationResult, IdentityPublicKeyInfo, } from './types.js'; -import type { BridgeState } from './types.js'; +import { + E2E_MOCK_DPNS_WIF, + E2E_MOCK_IDENTITY_ID, + E2E_MOCK_MANAGE_WIF, +} from './e2e-mock-constants.js'; // Global state let state: BridgeState; let insightClient: InsightClient; let dapiClient: DAPIClient; -const E2E_MOCK_IDENTITY_ID = '11111111111111111111111111111111111111111111'; -const E2E_MOCK_DPNS_WIF = 'cMockDpnsPrivateKeyWif'; -const E2E_MOCK_MANAGE_WIF = 'cMockManagePrivateKeyWif'; +const E2E_MOCK_ADVANCE_TIMEOUT_MS = 30000; +const E2E_MOCK_BUILD_ENABLED = !import.meta.env.PROD; type E2EMockWindow = Window & { __e2eMockAdvance?: () => void }; function isE2EMockMode(): boolean { + if (!E2E_MOCK_BUILD_ENABLED) { + return false; + } const urlParams = new URLSearchParams(window.location.search); return urlParams.get('e2e') === 'mock'; } @@ -121,7 +129,7 @@ function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } -function createE2EMockUtxo(): import('./types.js').UTXO { +function createE2EMockUtxo(): UTXO { return { txid: 'a'.repeat(64), vout: 0, @@ -159,11 +167,13 @@ function createE2EMockDpnsAvailability(entries: DpnsUsernameEntry[]): DpnsUserna } const lower = entry.label.toLowerCase(); + const normalizedLabel = entry.normalizedLabel || lower; const isAvailable = !lower.includes('taken'); return { ...entry, + normalizedLabel, isAvailable, - isContested: isAvailable ? isContestedUsername(lower) : undefined, + isContested: isAvailable ? isContestedUsername(normalizedLabel) : undefined, status: isAvailable ? 'available' : 'taken', }; }); @@ -184,13 +194,28 @@ function clearE2EMockAdvanceHook(): void { (window as E2EMockWindow).__e2eMockAdvance = undefined; } -function waitForE2EMockAdvance(): Promise { - return new Promise((resolve) => { +function waitForE2EMockAdvance(timeoutMs = E2E_MOCK_ADVANCE_TIMEOUT_MS): Promise { + return new Promise((resolve, reject) => { const mockWindow = window as E2EMockWindow; - mockWindow.__e2eMockAdvance = () => { - mockWindow.__e2eMockAdvance = undefined; + let timeoutId = 0; + + const cleanup = () => { + window.clearTimeout(timeoutId); + if (mockWindow.__e2eMockAdvance === resolveAdvance) { + mockWindow.__e2eMockAdvance = undefined; + } + }; + + const resolveAdvance = () => { + cleanup(); resolve(); }; + + mockWindow.__e2eMockAdvance = resolveAdvance; + timeoutId = window.setTimeout(() => { + cleanup(); + reject(new Error(`Timed out waiting for E2E mock advance after ${timeoutMs}ms`)); + }, timeoutMs); }); } @@ -1070,6 +1095,7 @@ async function startTopUp() { await delay(120); updateState(setOneTimeKeyPair(state, assetLockKeyPair, depositAddress)); updateState(setStep(state, 'detecting_deposit')); + clearE2EMockAdvanceHook(); await waitForE2EMockAdvance(); updateState(setUtxoDetected(state, createE2EMockUtxo())); await delay(120); @@ -1182,6 +1208,9 @@ async function startTopUp() { updateState(setTopUpComplete(state)); } catch (error) { + if (isE2EMockMode()) { + clearE2EMockAdvanceHook(); + } console.error('Top-up error:', error); updateState(setError(state, error instanceof Error ? error : new Error(String(error)))); } @@ -1325,7 +1354,9 @@ async function startBridge() { downloadKeyBackup(state); } catch (error) { - clearE2EMockAdvanceHook(); + if (isE2EMockMode()) { + clearE2EMockAdvanceHook(); + } console.error('Bridge error:', error); updateState(setError(state, error instanceof Error ? error : new Error(String(error)))); } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// From 250efc810e8cc67b0e23a7fda7174aed0a9a4d8f Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Fri, 20 Feb 2026 21:46:38 -0600 Subject: [PATCH 10/11] fix(e2e): apply remaining CodeRabbit follow-ups --- .gitignore | 1 + e2e/deterministic.spec.ts | 2 +- src/main.ts | 8 +++++++- src/types.ts | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dc7129f..d512575 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ .DS_Store playwright-report/ test-results/ +.env.playwright-live diff --git a/e2e/deterministic.spec.ts b/e2e/deterministic.spec.ts index 543040b..935e4f1 100644 --- a/e2e/deterministic.spec.ts +++ b/e2e/deterministic.spec.ts @@ -85,7 +85,7 @@ test.describe('Deterministic UI E2E (mock mode)', () => { await expect(page.getByText('Manage Keys')).toBeVisible(); await page.click('#add-manage-key-btn'); - await page.locator('.manage-disable-key-checkbox').first().check(); + await page.locator('.manage-disable-key-checkbox').first().click({ force: true }); await expect(page.getByText('Will add 1 key, disable 1 key')).toBeVisible(); await page.click('#apply-manage-btn'); diff --git a/src/main.ts b/src/main.ts index 0df5335..4215ba6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,6 +101,7 @@ import type { DpnsUsernameEntry, DpnsRegistrationResult, IdentityPublicKeyInfo, + E2EMockWindow, } from './types.js'; import { E2E_MOCK_DPNS_WIF, @@ -115,7 +116,6 @@ let dapiClient: DAPIClient; const E2E_MOCK_ADVANCE_TIMEOUT_MS = 30000; const E2E_MOCK_BUILD_ENABLED = !import.meta.env.PROD; -type E2EMockWindow = Window & { __e2eMockAdvance?: () => void }; function isE2EMockMode(): boolean { if (!E2E_MOCK_BUILD_ENABLED) { @@ -1036,6 +1036,12 @@ function setupEventListeners(container: HTMLElement) { const refreshedState = resetManageStateAndRefresh(state); updateState(refreshedState); + if (isE2EMockMode()) { + await delay(30); + updateState(setManageIdentityFetched(refreshedState, createE2EMockIdentityKeys())); + return; + } + // Refetch identity keys from the network const targetId = refreshedState.targetIdentityId; if (targetId) { diff --git a/src/types.ts b/src/types.ts index 4b12020..4b9df1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,8 @@ export interface KeyPair { publicKey: Uint8Array; } +export type E2EMockWindow = Window & { __e2eMockAdvance?: () => void }; + export interface UTXO { txid: string; vout: number; From 60a746d4a4294722ee56ba2dfc74eced85590445 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sat, 21 Feb 2026 18:08:47 -0600 Subject: [PATCH 11/11] fix(e2e): use AssetLockProofData objects in mock flows instead of plain strings --- src/main.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4215ba6..16b8ea4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1109,7 +1109,11 @@ async function startTopUp() { await delay(120); updateState(setTransactionBroadcast(state, 'b'.repeat(64))); await delay(120); - updateState(setInstantLockReceived(state, new Uint8Array([1, 2, 3, 4]), 'c0ffee')); + updateState(setInstantLockReceived(state, new Uint8Array([1, 2, 3, 4]), { + transactionBytes: new Uint8Array([0xc0, 0xff, 0xee]), + instantLockBytes: new Uint8Array([1, 2, 3, 4]), + outputIndex: 0, + })); updateState(setStep(state, 'topping_up')); await delay(120); updateState(setTopUpComplete(state)); @@ -1244,7 +1248,11 @@ async function startBridge() { await delay(120); updateState(setTransactionBroadcast(state, 'd'.repeat(64))); await delay(120); - updateState(setInstantLockReceived(state, new Uint8Array([5, 6, 7, 8]), 'beef')); + updateState(setInstantLockReceived(state, new Uint8Array([5, 6, 7, 8]), { + transactionBytes: new Uint8Array([0xbe, 0xef]), + instantLockBytes: new Uint8Array([5, 6, 7, 8]), + outputIndex: 0, + })); await delay(120); updateState(setIdentityRegistered(state, E2E_MOCK_IDENTITY_ID)); return;