Files
9edfdc94bb feat(web): one-shot clickthrough tests with CI video preview (#8043)
* feat(web): add Playwright one-shots suite, config project, and example clickthrough

Introduce a per-PR "one-shots" Playwright suite for complete happy-path
clickthroughs. Adds a dedicated 'one-shots' project that always records
video, gates the local webServer so the suite can target a deployed
preview via PLAYWRIGHT_BASE_URL, excludes one-shots from the default
chromium project, adds a pw:test:oneshot script, and an example
read-only dashboard clickthrough. Also fixes a Playwright >=1.49 fixture
signature in test.fixture.ts.

* tests: collapse one-shot dashboard example into a single clickthrough

A one-shot is a single complete happy-path clickthrough that produces
one cohesive video per run (later converted to a GIF in the PR), so the
example is one sequential test rather than three separate ones.

* tests: reuse homePage.goto and anchor balances URL regex in one-shot example

* chore(web): add video_to_gif helper to convert one-shot clickthrough to GIF and MP4

* chore(ci): record one-shot clickthrough against PR preview and post GIF

* fix(ci): correct video_to_gif.sh path in one-shot job

The script lives at apps/web/scripts/github/, so from working-directory
apps/web/e2e it must be invoked as ../scripts/github/video_to_gif.sh.
The previous ./scripts path did not exist, which would have made every
PR fall through to the 'no video produced' comment. Also make the
preview wait-loop exit 0 explicitly after timeout.

* feat(web): add local one-shot record wrapper and document the workflow

Add pw:oneshot:record (scripts/record-oneshot.sh) so authors and AI
agents can record + preview the one-shot clickthrough locally before
opening a PR, reusing the shared video_to_gif.sh helper. Document the
one-shots suite in the e2e README and AGENTS.md, and add a PR-template
checklist item. CI remains the authoritative recorder/poster of the GIF.

* fix(web): pin bare pw:test to chromium project so it skips one-shots

Without an explicit --project, Playwright instantiates every project,
including one-shots, which targets the local dev server and would
fail/hang for anyone running the bare pw:test. The grep-based scripts
are unaffected.

* feat(web): add Playwright wallet-connect and SiWE helpers for e2e

Add reusable wallet support to the e2e framework: a credentials util
reading CYPRESS_WALLET_CREDENTIALS, a WalletPage page object that
connects the private-key web3-onboard module via the connect UI and
performs SiWE login, plus walletPage/credentials fixtures and a parser
test. Enables one-shot clickthroughs to connect a wallet.

* feat(web): add pay-later Safe creation one-shot demo with wallet connect

Replace the read-only dashboard example with a wallet-connected demo
one-shot that clicks through the new-Safe creation wizard ending in
'Pay later' (counterfactual, no on-chain tx). It exercises connectWallet
and SiWE (selecting Pay later triggers the PK-module-signed sign-in).
Pass CYPRESS_WALLET_CREDENTIALS to the one-shot CI job and document the
wallet-connected one-shot pattern.

* tests: harden pay-later one-shot with wizard-step checkpoints

Wait for Next to enable on the set-name step (network resolves from the
wallet) and for the owners-step form before advancing, reducing
flakiness on a live preview. Clarify the SiWE helper doc.

* fix(web): make one-shot wallet flow run in CI and address review

- Install ffmpeg in the one-shot CI job (ubuntu-24.04 has none) so the
  video actually converts.
- Harden connectWallet: target the visible connect-wallet-btn and retry
  until the onboard modal opens (the hidden header button was swallowing
  the open on /welcome/accounts).
- video_to_gif.sh: use find -exec so no .webm yields a clear error
  instead of picking an arbitrary file; check ffmpeg presence.
- credentials: only OWNER_4_PRIVATE_KEY is required, validate field
  types; add parser test cases.
- SiWE waits now require a successful POST /v1/auth/verify (not any
  response).
- Gate the GIF PR comment on the S3 upload succeeding; fallback covers
  upload failure too.

* fix(web): open onboard reliably, dismiss cookie banner, dedupe GIF frames

- connectWallet: click a single visible connect-wallet-btn and key the
  open-retry off the shadow-DOM 'Private key' button (the onboard-v2 host
  stays hidden), fixing the strict-mode click that was swallowed and left
  the modal closed.
- Seed the correct terms version in the safePage fixture (the app gates
  consent on termsVersion === '1.3') and add acceptCookies() to dismiss
  the banner if it still shows.
- video_to_gif.sh: mpdecimate + re-timed PTS so static page dwells
  collapse instead of producing a long static GIF.

* tests: wait on URL for pay-later creation success with generous timeout

The counterfactual create persists to the backend before routing to the
new Safe; the persist can be slow on staging. Assert the /home?safe URL
change (the definitive success signal) with a 45s timeout instead of the
short default wait on the success dialog, and dismiss the dialog only if
shown.

* chore: share Stop hooks so Claude Code won't forget tests

Add a checked-in .claude/settings.json (applies to every contributor's
Claude Code) wiring two end-of-turn Stop hooks:
- verify:changed (the one AGENTS.md documented but was configured
  nowhere), now also honouring the documented SKIP_VERIFY=1 escape
  (added to scripts/verify.mjs).
- .claude/hooks/check-tests.mjs: advisory, never-blocking reminders that
  fire at most once per new change-set when web code lacks a one-shot
  clickthrough, or when significant source lacks a colocated unit test.
  Disable with SKIP_TEST_REMINDERS=1.

Document both in AGENTS.md.

* tests: end pay-later one-shot at creation-initiated (green, preview-safe)

Counterfactual creation persists to a backend not available on ephemeral
PR-preview deploys (the create button spins indefinitely), so asserting
the success screen was perpetually flaky. Assert the flow up to creation
being initiated (connect -> wizard -> SiWE -> Pay later -> create click ->
button enters creating state). The recording still captures the full
clickthrough including the create click.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-09 17:47:54 +02:00

74 lines
3.0 KiB
TypeScript

/**
* Wallet credentials parser tests — pure, no browser.
*
* Verifies getWalletCredentials parsing/validation. The raw JSON is passed via
* the `raw` param so these tests never mutate process.env.
*/
import { test, expect } from '../../src/fixtures/test.fixture'
import { getWalletCredentials, getDefaultSignerKey } from '../../src/data/credentials'
// Fake but well-formed hex keys/addresses — not real secrets.
const VALID_CREDS = JSON.stringify({
OWNER_1_PRIVATE_KEY: '0x1111111111111111111111111111111111111111111111111111111111111111',
OWNER_2_PRIVATE_KEY: '0x2222222222222222222222222222222222222222222222222222222222222222',
OWNER_4_PRIVATE_KEY: '0x4444444444444444444444444444444444444444444444444444444444444444',
OWNER_1_WALLET_ADDRESS: '0x1111111111111111111111111111111111111111',
OWNER_2_WALLET_ADDRESS: '0x2222222222222222222222222222222222222222',
})
test.describe('Wallet credentials parser', { tag: '@api' }, () => {
test('parses valid JSON into a typed object', () => {
const creds = getWalletCredentials(VALID_CREDS)
expect(creds.OWNER_4_PRIVATE_KEY).toBe('0x4444444444444444444444444444444444444444444444444444444444444444')
expect(creds.OWNER_1_WALLET_ADDRESS).toMatch(/^0x[a-fA-F0-9]{40}$/)
})
test('allows the optional owner fields to be absent', () => {
const onlySigner = JSON.stringify({
OWNER_4_PRIVATE_KEY: '0x4444444444444444444444444444444444444444444444444444444444444444',
})
const creds = getWalletCredentials(onlySigner)
expect(creds.OWNER_4_PRIVATE_KEY).toBe('0x4444444444444444444444444444444444444444444444444444444444444444')
expect(creds.OWNER_1_PRIVATE_KEY).toBeUndefined()
expect(creds.OWNER_2_WALLET_ADDRESS).toBeUndefined()
})
test('throws when OWNER_4_PRIVATE_KEY is present but not a string', () => {
const nonStringSigner = JSON.stringify({
OWNER_4_PRIVATE_KEY: 1234,
})
expect(() => getWalletCredentials(nonStringSigner)).toThrow(/OWNER_4_PRIVATE_KEY/)
})
test('getDefaultSignerKey returns OWNER_4_PRIVATE_KEY', () => {
const creds = getWalletCredentials(VALID_CREDS)
expect(getDefaultSignerKey(creds)).toBe(creds.OWNER_4_PRIVATE_KEY)
})
test('throws mentioning CYPRESS_WALLET_CREDENTIALS when raw is undefined', () => {
expect(() => getWalletCredentials(undefined)).toThrow(/CYPRESS_WALLET_CREDENTIALS/)
})
test('throws mentioning CYPRESS_WALLET_CREDENTIALS when raw is empty', () => {
expect(() => getWalletCredentials(' ')).toThrow(/CYPRESS_WALLET_CREDENTIALS/)
})
test('throws on malformed JSON', () => {
expect(() => getWalletCredentials('{ not valid json')).toThrow(/Failed to parse/)
})
test('throws when OWNER_4_PRIVATE_KEY is missing', () => {
const missingSigner = JSON.stringify({
OWNER_1_PRIVATE_KEY: '0x1111111111111111111111111111111111111111111111111111111111111111',
OWNER_1_WALLET_ADDRESS: '0x1111111111111111111111111111111111111111',
})
expect(() => getWalletCredentials(missingSigner)).toThrow(/OWNER_4_PRIVATE_KEY/)
})
})