From 9b6e994bc5f513c2984e31090924646e8d64d876 Mon Sep 17 00:00:00 2001 From: Gorka Date: Tue, 5 May 2026 13:43:25 -0300 Subject: [PATCH 1/3] feat(recording): single-pulse ring + 2s pre-click delay Telegraph each automated click with one 700ms ring pulse followed by a 2s pause before the click fires. The global pointerdown listener no longer double-rings clicks routed through clickWithPause (suppress flag). Also paces the Connect Wallet, Freighter modal pick, and Sign In clicks in the shared auth fixture, gated on __moonlightClickHighlight so verification runs are unaffected. Spec 01 holds 3s on the assets step and routes the final done-btn through clickWithPause. --- playwright/fixtures/auth.ts | 35 +++++++++++++++ .../playwright/fixtures/click-highlight.ts | 13 ++++++ recording/playwright/fixtures/pacing.ts | 21 +++++---- .../specs/01-council-onboard.spec.ts | 45 ++++++++++++------- 4 files changed, 91 insertions(+), 23 deletions(-) diff --git a/playwright/fixtures/auth.ts b/playwright/fixtures/auth.ts index 40b6985..9222e36 100644 --- a/playwright/fixtures/auth.ts +++ b/playwright/fixtures/auth.ts @@ -16,6 +16,38 @@ import { withWalletApproval, } from "./freighter"; +/** + * If the recording click-highlight init script is loaded on this page, + * spawn a ring at the locator's center and wait 2s before returning so + * the demo viewer can register the action. No-op in verification tests. + */ +async function paceForRecording(page: Page, selector: string): Promise { + const isRecording = await page.evaluate(() => + !!(globalThis as unknown as { __moonlightClickHighlight?: boolean }) + .__moonlightClickHighlight + ).catch(() => false); + if (!isRecording) return; + try { + const box = await page.locator(selector).first().boundingBox(); + if (!box) return; + await page.evaluate( + ({ x, y }: { x: number; y: number }) => { + (globalThis as unknown as { + __moonlightSpawnRing?: (x: number, y: number) => void; + }).__moonlightSpawnRing?.(x, y); + }, + { x: box.x + box.width / 2, y: box.y + box.height / 2 }, + ); + await page.waitForTimeout(2000); + await page.evaluate(() => { + (globalThis as unknown as { __moonlightSuppressNextAutoRing?: boolean }) + .__moonlightSuppressNextAutoRing = true; + }); + } catch { + // Best effort — fall through to the click. + } +} + /** * Connect Freighter wallet on a login page. * @@ -35,6 +67,7 @@ export async function connectWallet( await page.waitForSelector(connectBtnSelector, { timeout: 15_000 }); // Step 1: Click the connect button to open the wallet picker modal + await paceForRecording(page, connectBtnSelector); await page.click(connectBtnSelector); await page.waitForTimeout(1000); @@ -47,6 +80,7 @@ export async function connectWallet( if ( await freighterOption.isVisible({ timeout: 3_000 }).catch(() => false) ) { + await paceForRecording(page, "text=Freighter"); await freighterOption.click(); } else { // Fallback: look inside the web component's shadow DOM @@ -82,6 +116,7 @@ export async function signIn( // Listen for popup 1 before clicking const popup1Promise = context.waitForEvent("page", { timeout: 30_000 }); + await paceForRecording(page, signInBtnSelector); await page.click(signInBtnSelector); const popup1 = await popup1Promise; diff --git a/recording/playwright/fixtures/click-highlight.ts b/recording/playwright/fixtures/click-highlight.ts index 36fcdfd..60cdcd3 100644 --- a/recording/playwright/fixtures/click-highlight.ts +++ b/recording/playwright/fixtures/click-highlight.ts @@ -52,6 +52,19 @@ const INIT_SCRIPT = ` } window.__moonlightSpawnRing = spawnRing; + + // Auto-spawn a ring on every click, in capture phase so we never miss one + // (handlers that stopPropagation can't suppress us). Covers bare .click() + // calls that don't go through clickWithPause. + // clickWithPause sets __moonlightSuppressNextAutoRing right before its + // own click so we don't double-ring (manual ring + auto ring on click). + document.addEventListener("pointerdown", (e) => { + if (window.__moonlightSuppressNextAutoRing) { + window.__moonlightSuppressNextAutoRing = false; + return; + } + spawnRing(e.clientX, e.clientY); + }, true); })(); `; diff --git a/recording/playwright/fixtures/pacing.ts b/recording/playwright/fixtures/pacing.ts index 1d9a5ee..f95b08a 100644 --- a/recording/playwright/fixtures/pacing.ts +++ b/recording/playwright/fixtures/pacing.ts @@ -49,9 +49,12 @@ export async function typeSlowly( } /** - * Scroll into view + telegraph the target with a focus ring + click. - * The pre-click ring lands the viewer's eye on the target before the click - * actually fires, so the action reads cleanly on the recording. + * Scroll into view + spawn one sustained ring + 2s wait + click. + * + * The ring CSS animation in click-highlight.ts holds visible for ~2s + * before fading, so a single spawn telegraphs the target continuously. + * The suppress flag stops the global pointerdown listener in + * click-highlight.ts from spawning a second ring at click time. */ export async function clickWithPause(locator: Locator): Promise { await locator.scrollIntoViewIfNeeded(); @@ -65,17 +68,19 @@ export async function clickWithPause(locator: Locator): Promise { const w = globalThis as unknown as { __moonlightSpawnRing?: (x: number, y: number) => void; }; - if (typeof w.__moonlightSpawnRing === "function") { - w.__moonlightSpawnRing(x, y); - } + w.__moonlightSpawnRing?.(x, y); }, { x: box.x + box.width / 2, y: box.y + box.height / 2 }, ); - await page.waitForTimeout(1000); + await page.waitForTimeout(2000); } } catch { - // Detached / cross-origin frame — fall through to the click. + // Detached / cross-origin — fall through to the click. } + await page.evaluate(() => { + (globalThis as unknown as { __moonlightSuppressNextAutoRing?: boolean }) + .__moonlightSuppressNextAutoRing = true; + }); await locator.click(); await beat(page); } diff --git a/recording/playwright/specs/01-council-onboard.spec.ts b/recording/playwright/specs/01-council-onboard.spec.ts index 6007201..332fcb4 100644 --- a/recording/playwright/specs/01-council-onboard.spec.ts +++ b/recording/playwright/specs/01-council-onboard.spec.ts @@ -28,7 +28,7 @@ import { import { withWalletApproval } from "../../../playwright/fixtures/freighter"; import { getUrls } from "../../../playwright/helpers/urls"; import { loadRunEnv, updateRunEnv } from "../helpers/run-env"; -import { holdAfterSuccess } from "../fixtures/pacing"; +import { clickWithPause, holdAfterSuccess, typeSlowly } from "../fixtures/pacing"; import { addClickHighlight } from "../fixtures/click-highlight"; const COUNCIL_NAME = "Moonlight Demo"; @@ -59,26 +59,35 @@ test("01 — council onboarding", async () => { await verifyAuthenticated(councilPage, "nav", 30_000); // Beat 2 — create council - await councilPage.locator("#create-btn, #new-council-btn").first().click(); + await clickWithPause( + councilPage.locator("#create-btn, #new-council-btn").first(), + ); await councilPage.waitForSelector("#council-name", { timeout: 10_000 }); - await councilPage.fill("#council-name", COUNCIL_NAME); - await councilPage.fill("#council-description", COUNCIL_DESCRIPTION); - await councilPage.fill("#council-email", COUNCIL_EMAIL); + await typeSlowly(councilPage.locator("#council-name"), COUNCIL_NAME); + await typeSlowly( + councilPage.locator("#council-description"), + COUNCIL_DESCRIPTION, + ); + await typeSlowly(councilPage.locator("#council-email"), COUNCIL_EMAIL); const jurisdictionPicker = councilPage.locator("#jurisdiction-picker"); if ( await jurisdictionPicker.isVisible({ timeout: 3_000 }).catch(() => false) ) { - await councilPage.fill("#jurisdiction-filter", "United States"); + await typeSlowly( + councilPage.locator("#jurisdiction-filter"), + "United States", + ); await councilPage.waitForTimeout(500); const opt = councilPage.locator( "#jurisdiction-list .jurisdiction-option, .jurisdiction-option", ).first(); if (await opt.isVisible({ timeout: 2_000 }).catch(() => false)) { - await opt.click(); + await clickWithPause(opt); } } - await councilPage.click("#next-btn"); + await clickWithPause(councilPage.locator("#next-btn")); + await councilPage.waitForTimeout(2000); // Beat 3 — deploy contracts (multiple signing popups) await councilPage.waitForSelector("#create-btn", { timeout: 10_000 }); @@ -106,27 +115,33 @@ test("01 — council onboarding", async () => { }; adminCtx.context.on("page", approvePopup); - await councilPage.click("#create-btn"); + await clickWithPause(councilPage.locator("#create-btn")); await councilPage.waitForSelector("#fund-amount", { timeout: 180_000 }); adminCtx.context.off("page", approvePopup); console.log(`Council deploy: ${popupsApproved} signing popups approved`); if (popupError) throw popupError; // Beat 4 — fund treasury - await councilPage.fill("#fund-amount", "10"); + await typeSlowly(councilPage.locator("#fund-amount"), "10"); await withWalletApproval(adminCtx.context, councilPage, async () => { - await councilPage.click("#fund-btn"); + await clickWithPause(councilPage.locator("#fund-btn")); }); await councilPage.waitForSelector("#next-btn:not([disabled])", { timeout: 60_000, }); - await councilPage.click("#next-btn"); + await clickWithPause(councilPage.locator("#next-btn")); + await councilPage.waitForTimeout(2000); - // Beat 5 — assets (XLM auto-enabled) + // Beat 5 — assets (XLM auto-enabled). Hold so the viewer can read the + // assets list before we move on. await councilPage.waitForSelector("#continue-btn, #next-btn", { timeout: 10_000, }); - await councilPage.locator("#continue-btn, #next-btn").first().click(); + await councilPage.waitForTimeout(3000); + await clickWithPause( + councilPage.locator("#continue-btn, #next-btn").first(), + ); + await councilPage.waitForTimeout(2000); // Beat 6 — invite step → capture council/channel IDs await councilPage.waitForSelector("#done-btn", { timeout: 10_000 }); @@ -170,7 +185,7 @@ test("01 — council onboarding", async () => { console.log(`CHANNEL_AUTH_ID=${channelAuthId}`); if (privacyChannelId) console.log(`PRIVACY_CHANNEL_ID=${privacyChannelId}`); - await councilPage.click("#done-btn"); + await clickWithPause(councilPage.locator("#done-btn")); await councilPage.waitForLoadState("networkidle"); await expect(councilPage.locator(`text=${COUNCIL_NAME}`).first()) .toBeVisible({ timeout: 15_000 }); From 62fca630098a7afcbfc9366af1282dc7ef4730b9 Mon Sep 17 00:00:00 2001 From: Gorka Date: Tue, 5 May 2026 14:02:16 -0300 Subject: [PATCH 2/3] feat(recording): pace every moonlight UI click in spec 02 Wraps create-pp / next / fund / discover / join / council-link / approve clicks with clickWithPause and replaces .fill() with typeSlowly so the provider create + join + approve flow paces identically to spec 01 in the recording. Adds scrollIntoViewIfNeeded inside paceForRecording so the Connect Wallet ring lands in-viewport on consoles (e.g. provider-console) where the button renders below the fold. --- playwright/fixtures/auth.ts | 4 +- .../02-provider-create-join-approve.spec.ts | 42 +++++++++++-------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/playwright/fixtures/auth.ts b/playwright/fixtures/auth.ts index 9222e36..0258eb6 100644 --- a/playwright/fixtures/auth.ts +++ b/playwright/fixtures/auth.ts @@ -28,7 +28,9 @@ async function paceForRecording(page: Page, selector: string): Promise { ).catch(() => false); if (!isRecording) return; try { - const box = await page.locator(selector).first().boundingBox(); + const locator = page.locator(selector).first(); + await locator.scrollIntoViewIfNeeded(); + const box = await locator.boundingBox(); if (!box) return; await page.evaluate( ({ x, y }: { x: number; y: number }) => { diff --git a/recording/playwright/specs/02-provider-create-join-approve.spec.ts b/recording/playwright/specs/02-provider-create-join-approve.spec.ts index b977f2a..a29f8db 100644 --- a/recording/playwright/specs/02-provider-create-join-approve.spec.ts +++ b/recording/playwright/specs/02-provider-create-join-approve.spec.ts @@ -23,7 +23,7 @@ import { import { withWalletApproval } from "../../../playwright/fixtures/freighter"; import { getUrls } from "../../../playwright/helpers/urls"; import { loadRunEnv, requireValue } from "../helpers/run-env"; -import { holdAfterSuccess } from "../fixtures/pacing"; +import { clickWithPause, holdAfterSuccess, typeSlowly } from "../fixtures/pacing"; import { addClickHighlight } from "../fixtures/click-highlight"; const PROVIDER_NAME = "Acme Privacy Provider"; @@ -60,26 +60,27 @@ test("02 — provider create + join + approve", async () => { await verifyAuthenticated(providerPage, "nav", 30_000); // Beat 2 — create provider - await providerPage - .locator("#create-pp-btn, button:has-text('Create')") - .first() - .click(); + await clickWithPause( + providerPage + .locator("#create-pp-btn, button:has-text('Create')") + .first(), + ); await providerPage.waitForSelector("#pp-name", { timeout: 10_000 }); - await providerPage.fill("#pp-name", PROVIDER_NAME); - await providerPage.fill("#pp-email", PROVIDER_EMAIL); - await providerPage.click("#next-btn"); + await typeSlowly(providerPage.locator("#pp-name"), PROVIDER_NAME); + await typeSlowly(providerPage.locator("#pp-email"), PROVIDER_EMAIL); + await clickWithPause(providerPage.locator("#next-btn")); // Beat 3 — fund PP operator await providerPage.waitForSelector("#fund-amount", { timeout: 15_000 }); - await providerPage.fill("#fund-amount", "10"); + await typeSlowly(providerPage.locator("#fund-amount"), "10"); await withWalletApproval(ppCtx.context, providerPage, async () => { - await providerPage.click("#fund-btn"); + await clickWithPause(providerPage.locator("#fund-btn")); }); await providerPage.waitForSelector("#next-btn:not([disabled])", { timeout: 60_000, }); - await providerPage.click("#next-btn"); + await clickWithPause(providerPage.locator("#next-btn")); await providerPage.waitForLoadState("networkidle"); // Beat 4 — request join @@ -94,22 +95,25 @@ test("02 — provider create + join + approve", async () => { await providerPage.waitForLoadState("networkidle"); const joinModalBtn = providerPage.locator(".join-council-btn").first(); if (await joinModalBtn.isVisible({ timeout: 5_000 }).catch(() => false)) { - await joinModalBtn.click(); + await clickWithPause(joinModalBtn); await providerPage.waitForTimeout(1000); } } const urlInput = providerPage.locator("#council-url, #jc-url").first(); await urlInput.waitFor({ state: "visible", timeout: 10_000 }); - await urlInput.fill(`${urls.councilApi}?council=${councilId}`); + await typeSlowly(urlInput, `${urls.councilApi}?council=${councilId}`); - await providerPage.locator("#discover-btn, #jc-discover-btn").first() - .click(); + await clickWithPause( + providerPage.locator("#discover-btn, #jc-discover-btn").first(), + ); await providerPage.waitForSelector("#council-info, #jc-info, #jc-confirm", { state: "visible", timeout: 15_000, }); - await providerPage.locator("#join-btn, #jc-join-btn").first().click(); + await clickWithPause( + providerPage.locator("#join-btn, #jc-join-btn").first(), + ); await providerPage.waitForLoadState("networkidle"); await providerPage.waitForTimeout(2000); @@ -127,7 +131,9 @@ test("02 — provider create + join + approve", async () => { await councilPage.waitForLoadState("networkidle"); await loginWithFreighter(adminCtx.context, councilPage); await verifyAuthenticated(councilPage, "nav", 30_000); - await councilPage.locator(`a[href*="${councilId}"]`).first().click(); + await clickWithPause( + councilPage.locator(`a[href*="${councilId}"]`).first(), + ); await councilPage.waitForLoadState("networkidle"); const requestedRow = councilPage.locator( @@ -140,7 +146,7 @@ test("02 — provider create + join + approve", async () => { await approveBtn.waitFor({ state: "visible", timeout: 5_000 }); await withWalletApproval(adminCtx.context, councilPage, async () => { - await approveBtn.click(); + await clickWithPause(approveBtn); }); // Beat 6 — verify active From d2cb04368fa7a9c0de90532c4c63af56fb50d26d Mon Sep 17 00:00:00 2001 From: Gorka Date: Wed, 6 May 2026 09:52:39 -0300 Subject: [PATCH 3/3] feat(recording): consolidate spec 03 + public-view wrap-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 03 is now a single continuous Bob+Alice flow: Bob receive → Alice deposit + send → Bob withdraw. Both wallets toggle back to public view at the end so the recording surfaces the on-chain XLM balances proving deposit + withdraw landed. Also: - openWalletPopup injects CSS to override the wallet's hardcoded h-[600px] so the Add Channel form no longer shows a scrollbar. - deposit waits on :not([disabled]) Review button so validation can catch up to the last keystroke. - Click the Copy MLXDR button on Bob's receive screen as a viewer cue. - README updated to reflect the 4-spec layout, runtime, and consolidated 03. --- recording/playwright/README.md | 57 ++++++++++--------- .../playwright/fixtures/browser-wallet.ts | 39 ++++++++++++- .../specs/03-private-transfer.spec.ts | 15 ++++- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/recording/playwright/README.md b/recording/playwright/README.md index cb8767e..3da8787 100644 --- a/recording/playwright/README.md +++ b/recording/playwright/README.md @@ -20,23 +20,21 @@ A single recording run goes through these phases, in order: without seed injection so the full UI onboarding flow records. 3. **Specs run in order** — each spec is a separate Playwright invocation that loads `run.env` for shared state. Sections 01 and 02 backfill the contract - IDs that 03a/b/c and 04 consume. + IDs that 03 and 04 consume. 4. **Outputs** — videos land under `runs//videos//.webm`. Each section is its own file; cut + dub in post. ## Sections -Run order matters: 01 → 02 → 03a → 03b → 03c → 04. +Run order matters: 01 → 02 → 03 → 04. -| # | Spec | What records | Wallet | Reads from run.env | Writes to run.env | -| --- | ----------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------- | -| 01 | `01-council-onboard.spec.ts` | council-console: create council, deploy contracts, fund | Freighter | ADMIN_*, COUNCIL_CONSOLE_URL | CHANNEL_AUTH_ID, PRIVACY_CHANNEL_ID | -| 02 | `02-provider-create-join-approve.spec.ts` | provider-console + council-console approval | Freighter | `PP_*`, `ADMIN_*`, CHANNEL_AUTH_ID | — | -| 03a | `03a-alice-deposit-send.spec.ts` | browser-wallet: onboard, add channel, connect provider, deposit, send | browser-wallet | ALICE_*, PRIVACY_CHANNEL_ID, PROVIDER_PLATFORM_URL | bob-mlxdr.txt artifact | -| 03b | `03b-bob-receive.spec.ts` | browser-wallet: onboard, receive view (captures MLXDR) | browser-wallet | BOB_*, PRIVACY_CHANNEL_ID, PROVIDER_PLATFORM_URL | bob-mlxdr.txt artifact | -| 03c | `03c-alice-withdraw.spec.ts` | browser-wallet: withdraw | browser-wallet | ALICE_*, PRIVACY_CHANNEL_ID | — | -| 04 | `04-dashboard-tour.spec.ts` | dashboard: council list, channel detail, provider, activity | none (uses launchPersistentContext to keep recording rig consistent) | DASHBOARD_URL, COUNCIL_PLATFORM_URL, CHANNEL_AUTH_ID | — | +| # | Spec | What records | Wallet | Reads from run.env | Writes to run.env | +| --- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------- | +| 01 | `01-council-onboard.spec.ts` | council-console: create council, deploy contracts, fund | Freighter | ADMIN_*, COUNCIL_CONSOLE_URL | CHANNEL_AUTH_ID, PRIVACY_CHANNEL_ID | +| 02 | `02-provider-create-join-approve.spec.ts` | provider-console + council-console approval | Freighter | `PP_*`, `ADMIN_*`, CHANNEL_AUTH_ID | — | +| 03 | `03-private-transfer.spec.ts` | browser-wallet (Bob + Alice in one continuous flow): Bob receive → Alice onboard + deposit + send → Bob withdraw → both wallets flip back to public view to surface on-chain XLM as the closing beat | browser-wallet | ALICE_*, BOB_*, PRIVACY_CHANNEL_ID, PROVIDER_PLATFORM_URL | — | +| 04 | `04-dashboard-tour.spec.ts` | dashboard: council list, council detail, channels + providers, recent activity, transactions, totals | none (uses launchPersistentContext to keep recording rig consistent) | DASHBOARD_URL, COUNCIL_PLATFORM_URL, CHANNEL_AUTH_ID | — | `run.env` (under `recording/runs//`) is the single shared-state mechanism between specs: @@ -45,9 +43,9 @@ mechanism between specs: - Section 01 backfills `CHANNEL_AUTH_ID` and `PRIVACY_CHANNEL_ID`. - Sections 02-04 read those values. -Multi-line blobs (e.g. Bob's receive MLXDR) are persisted as -`runs//.txt` artifacts via `writeRunArtifact`/`readRunArtifact` so -they don't pollute `run.env`. +Section 03 keeps Bob's receive MLXDR in memory across the Bob-receive → +Alice-send sub-beats (the two wallets share one test), so no on-disk artifact +is needed. ## Setup @@ -74,7 +72,7 @@ npm run install:browsers RUN_ID= npm run record # One section -RUN_ID= npx playwright test specs/03a-alice-deposit-send.spec.ts +RUN_ID= npx playwright test specs/03-private-transfer.spec.ts ``` Videos land under: @@ -84,13 +82,11 @@ local-dev/recording/runs// ├── run.env ├── keys.txt ├── .env.seed.user{1,2} -├── bob-mlxdr.txt (only after 03b) └── videos/ ├── 01-council-onboard.spec.ts/.webm ├── 02-provider-create-join-approve.spec.ts/.webm - ├── 03a-alice-deposit-send.spec.ts/.webm - ├── 03b-bob-receive.spec.ts/.webm - ├── 03c-alice-withdraw.spec.ts/.webm + ├── 03-bob/.webm (Bob's wallet during section 03) + ├── 03-alice/.webm (Alice's wallet during section 03) └── 04-dashboard-tour.spec.ts/.webm ``` @@ -103,12 +99,13 @@ helper manages its own browser context outside this rig's `playwright.config.ts`, so Playwright's `video: "on"` setting does **not** apply — these specs run successfully but emit no `.webm` for the Freighter beats. -Sections 03a / 03b / 03c / 04 use the recording rig's own fixtures and do -produce `.webm` files. +Sections 03 / 04 use the recording rig's own fixtures and do produce `.webm` +files (one per wallet window for section 03 — Bob and Alice each get their +own video). -To capture the full six-section run end-to-end, screen-record the display while -`npm run record` executes. The rig already runs non-headless (`headless: false` -in `playwright.config.ts`), so every window is visible. +To capture the full four-section run end-to-end, screen-record the display +while `npm run record` executes. The rig already runs non-headless +(`headless: false` in `playwright.config.ts`), so every window is visible. ```bash # In one terminal — start your screen recorder (QuickTime "New Screen @@ -123,9 +120,10 @@ Tips: Chromium contexts (admin, pp, alice, bob) as separate windows; window-only capture will miss handoffs. - Each context renders at the configured viewport (default 1280x720). -- Total runtime is ~7 minutes for all six specs. +- Total runtime is ~11 minutes for all four specs (01: ~2.4m, 02: ~3.2m, 03: + ~5m, 04: ~40s). - Cursor + window chrome appear in the screen recording. The Playwright `.webm` - outputs (03a–04) intentionally do not, so use whichever source is right for + outputs (03–04) intentionally do not, so use whichever source is right for the section. ## Tunables @@ -163,6 +161,9 @@ the demo doesn't snap to the next step. ## Status -End-to-end validation runs against the local stack with all six specs passing -(`01`, `02`, `03a`, `03b`, `03c`, `04`). Section 04's tour beats -(scroll-to-council, drill-in, provider list, bundle activity) are still TODO. +End-to-end validation runs against the local stack with all four specs passing +(`01`, `02`, `03`, `04`). Section 03 was consolidated from the original +03a/03b/03c split into a single continuous Bob+Alice flow that ends with both +wallets toggled back to public view so the on-chain XLM balances confirm +deposit + withdraw landed. Section 04's tour beats (council list, drill-in, +channels, providers, recent activity, transactions, totals) are wired up. diff --git a/recording/playwright/fixtures/browser-wallet.ts b/recording/playwright/fixtures/browser-wallet.ts index a1b56c1..996b3a1 100644 --- a/recording/playwright/fixtures/browser-wallet.ts +++ b/recording/playwright/fixtures/browser-wallet.ts @@ -92,6 +92,9 @@ export const SEL = { // Confirmation page renders the MLXDR inside a labeled card. receiveMlxdrLabel: "text=Receiving Address (MLXDR)", receiveMlxdrValue: "span.font-mono", + // Copy MLXDR — clicking it telegraphs "this is what the sender needs". + receiveCopyButton: + 'button:has(svg.tabler-icon-copy), button:has-text("Copy")', // Private-view home: "Confidential Balance" label sits above the actual // figure. Polling its sibling for a non-zero number is how we tell the @@ -175,8 +178,16 @@ export async function openWalletPopup( extensionId: string, ): Promise { const page = await context.newPage(); + await page.setViewportSize({ width: 1280, height: 1080 }); await page.goto(`chrome-extension://${extensionId}/popup.html`); await page.waitForLoadState("domcontentloaded"); + // Override the wallet's hardcoded h-[600px] background so tall content + // doesn't show a scrollbar in recordings. + await page.addStyleTag({ + content: ` + #root > div { height: 100vh !important; min-height: 100vh !important; } + `, + }); await page.bringToFront(); return page; } @@ -283,6 +294,19 @@ export async function toggleToPrivateView(page: Page): Promise { await hold(page); } +/** Toggle home from private to public view. */ +export async function toggleToPublicView(page: Page): Promise { + await page.bringToFront(); + const toggle = page.locator(SEL.viewModeToggleByShield).first(); + if (await toggle.isVisible({ timeout: 5_000 }).catch(() => false)) { + await clickWithPause(toggle); + } + await page.locator(SEL.homePublicBalanceLabel).first().waitFor({ + timeout: 10_000, + }); + await hold(page); +} + export interface AddChannelOptions { contractId: string; channelName: string; @@ -414,7 +438,13 @@ export async function deposit(page: Page, opts: AmountOptions): Promise { // Defaults to deposit mode + Direct method. await typeSlowly(page.locator(SEL.rampAmountInput).first(), opts.amount); - await clickWithPause(page.locator(SEL.rampReviewDeposit).first()); + // Wait for the form to enable the Review button (validation can lag the + // last keystroke). Targets the enabled instance directly. + const reviewDeposit = page + .locator(`${SEL.rampReviewDeposit}:not([disabled])`) + .first(); + await reviewDeposit.waitFor({ state: "visible", timeout: 15_000 }); + await clickWithPause(reviewDeposit); // Review screen → Execute Transaction. const exec = page.locator(SEL.rampExecute).first(); @@ -482,6 +512,13 @@ export async function showReceive( .waitFor({ timeout: 30_000 }); await holdAfterSuccess(page); + // Click the Copy button so the viewer sees "this is what the sender needs" + // before we move on. Best-effort — skip if the button isn't surfaced. + const copyBtn = page.locator(SEL.receiveCopyButton).first(); + if (await copyBtn.isVisible({ timeout: 2_000 }).catch(() => false)) { + await clickWithPause(copyBtn); + } + try { const out = page.locator(SEL.receiveMlxdrValue).first(); if (await out.isVisible({ timeout: 5_000 }).catch(() => false)) { diff --git a/recording/playwright/specs/03-private-transfer.spec.ts b/recording/playwright/specs/03-private-transfer.spec.ts index ec7fef6..a71137d 100644 --- a/recording/playwright/specs/03-private-transfer.spec.ts +++ b/recording/playwright/specs/03-private-transfer.spec.ts @@ -28,6 +28,7 @@ import { send, showReceive, toggleToPrivateView, + toggleToPublicView, waitForConfidentialBalance, withdraw, } from "../fixtures/browser-wallet"; @@ -120,10 +121,18 @@ test("03 — private transfer (Bob receive → Alice deposit + send → Alice wi await closeReceiveConfirmation(bobWallet); await waitForConfidentialBalance(bobWallet, 24.99); - await withdraw(aliceWallet, { - amount: "70", - destinationAddress: env.ALICE_PK, + // Bob withdraws ~all of what he received (25 minus the receive fee) so + // his public XLM balance visibly grows — that's the on-chain proof the + // private transfer landed. + await withdraw(bobWallet, { + amount: "20", + destinationAddress: env.BOB_PK, }); + + // Closing beat: flip both wallets back to public view so the recording + // shows the on-chain XLM balances confirming deposit/withdraw worked. + await toggleToPublicView(aliceWallet); + await toggleToPublicView(bobWallet); } finally { if (aliceHandle) await closeWalletContext(aliceHandle); await closeWalletContext(bobHandle);