Skip to content

Headless E2E on deployed dev (test accounts + harness) โ€‹

How the two-account headless browser E2E runs against deployed dev (dev.ourlantern.app), and the exact steps to create a new test phone number when you need one. The first live user of this is the #144 block-sever flow (npm run e2e:block-sever).

Scope. This is on-demand verification, not CI. It drives the real deployed dev site with real Firestore writes, so it is deliberately kept out of the per-PR pipeline (too slow, too stateful, real data). Run it by hand when you want a full-stack confidence check.

The moving parts โ€‹

PieceWhat it isWhere it lives
HarnessPlaywright scripts (session.mjs, flow.mjs, runner)tooling/e2e/
Test credsphone/PIN per account, App Check token, UIDs~/.lantern-test-accounts.env (0600, never committed)
App Check debug tokenlets a headless browser sign in past reCAPTCHA Enterpriseregistered in Firebase Console, value in the env file
Prelude sandboxfakes the SMS OTP for test numbersPrelude dashboard (operator)
Admin (Firestore)reads/writes for teardown + assertionsApplication Default Credentials on the VM

Creating a NEW test phone number (do this in order) โ€‹

The whole thing hinges on ONE rule: the Prelude sandbox number must be character-for-character identical to the E.164 string the app sends. The app assumes US and turns a bare 10-digit number into +1XXXXXXXXXX. So a US test number 619-555-0004 MUST be registered as +16195550004 (with the 1), not +6195550004.

Why this bites: +6195550004 reads to Prelude as country code +61 (Australia), a different number, so it rejects the send as "Invalid Number" and the app shows "failed to create account." We lost hours to exactly this. Confirmed in code: normalizePhoneNumber in packages/shared/encryption/index.js prepends +1 to any bare 10-digit input.

Fast path (one command): once the sandbox number exists (step 1), run

bash
node tooling/e2e/onboard.mjs <bareUsPhone> <6-digit-pin>   # e.g. 6195550004 738261

It mints an invite directly (Admin SDK, no admin portal), runs the invite-gated signup headlessly with the App Check debug token, and prints the new TEST_B_PHONE/PIN/UID to paste into the env file. The PIN is yours to choose (6 digits, must pass validatePIN, i.e. not in WEAK_PINS). Note: the signup navigates away on the final submit, so a "Create my account button timeout" at the very end can be a false alarm; check whether the account was actually created by looking at the minted invite's usedBy (that value IS the new UID).

Manual path, step by step:

  1. Prelude โ†’ Sandboxed Numbers โ†’ Add Number. Enter the full E.164 with the US country code: +16195550004. Attempt code: 123456. (Every sandbox number uses OTP 123456.) Sandbox sends are free and never dispatch a real SMS.
  2. Generate an invite link. Signup is invite-gated. Either let onboard.mjs mint one, or write a userInvites/<token> doc directly (fields: createdAt, createdBy, expiresAt, usedAt:null, usedBy:null), or log into admin.dev.ourlantern.app as the Agent Probe admin (creds in ~/.lantern-agent-probe.env), Users page โ†’ "Generate Invite Link". (Never use the Agent Probe number as an app user; it is admin-only.)
  3. Rate limits exist but sandbox sends are free. auth-api (phoneOtp.js) still counts 5 sends per phone per hour / 5 per IP per 10 min, but the OTP is a static 123456 and no real SMS is sent, so there is no per-send cost, just the counter. Don't churn if you're near the cap.
  4. Sign up via signUp({ invite, phone, pin }) (session.mjs) or by hand. Phone-verify OTP is 123456. The account is real afterward and signs in with phone + PIN (no OTP on login).
  5. Record it. Add TEST_x_PHONE / TEST_x_PIN / TEST_x_UID to ~/.lantern-test-accounts.env. The UID is needed for teardown; onboard.mjs prints it, or read it from the invite's usedBy or any connections doc the account appears in (user1Id/user2Id).

App Check debug token โ€‹

Deployed dev enforces Firebase App Check (reCAPTCHA Enterprise). A headless browser has no reCAPTCHA, so every signed-in call 401s with "Missing X-Firebase-AppCheck header" unless you present a debug token:

  • Operator registers a UUID under Firebase Console โ†’ App Check โ†’ (web app) โ†’ Manage debug tokens.
  • Store the UUID as APPCHECK_DEBUG_TOKEN in the env file.
  • The harness injects it via self.FIREBASE_APPCHECK_DEBUG_TOKEN in a Playwright addInitScript (runs before page scripts), so the App Check SDK uses it instead of reCAPTCHA.
  • It bypasses attestation only, not auth. Low-privilege; still keep it out of git.

Running the block-sever E2E โ€‹

bash
npm run e2e:block-sever      # from repo root; needs the env file + ADC

What it does: teardown any prior connection/block between the two accounts โ†’ both sign in (geolocated to the venue) โ†’ both light a lantern โ†’ B waves โ†’ A accepts (chat opens) โ†’ A messages โ†’ A blocks B with "save a copy" โ†’ asserts in Firestore that the connection is blockClosed and its messages are gone, AND captures the "save a copy" .txt download and asserts it still holds the message (capture-then-sever: the client copy is taken BEFORE the server destroys the thread).

Re-runnability. teardownPair(uidA, uidB) deletes every connection between the two UIDs (plus messages), both users/{uid}/blocks/{peer} docs, and pending waves before the run starts. Without it, a prior run's connection blocks a fresh wave (already-connected) and a prior block hides the two from each other. It needs TEST_MAIN_UID and TEST_B_UID in the env file.

UID correctness matters (this bit us). A block hides the two accounts from each other in the venue view, so a wrong TEST_MAIN_UID/TEST_B_UID means teardown clears the wrong pair and a real block from the previous run survives, wedging the NEXT run at the wave step ("B never sees A's lantern"). Get the UIDs from a connections doc the pair actually appears in (user1Id/user2Id) and confirm which is which, do not assume user1 = MAIN. As a backstop, the runner also self-heals at the end: it clears the block using the connection's real user1Id/user2Id, so a stale env UID can't wedge future runs.

Gotchas baked into the harness:

  • Fresh browser context = fresh onboarding every run. Tutorial state is per-browser (localStorage), so each run hits (a) the 9-step dashboard tour and (b) a venue coach-mark ("Light your lantern here"). dismissTour and dismissVenueCoachMark handle both. A not-yet-lit account reaches the light form via the flame button โ†’ LanternHub โ†’ "Light Lantern", not the /light route directly.
  • Tall viewport. The block-confirmation modal is taller than the 720px headless default, which pushed the confirm button off-screen; the context uses a 1280x1400 viewport.
  • Venue + geofence. Dev venues are in San Diego. The runner uses Ginger's (00N4CsJJCL4GGiRBhl8w, 32.7117496,-117.160303); geolocation is set at the browser-context level, so it works regardless of the app's dev-only spoof helpers. Lighting requires being inside the ~100m geofence.
  • Feature test plan: docs/engineering/testing/runs/block-sever-144/README.md
  • Phone-input UX follow-up (default +1, reject bad country codes): issue #777
  • Browser-testing basics: the reference_browser_testing auto-memory

Built with VitePress