Files
bridge/e2e/tests/multi-tenant.spec.ts
zeekay f627f20da1 e2e: Playwright smoke + multi-tenant specs for tenant bridge SPA
Env-parametric specs covering bridge.lux.network (and bridge.zoo.network
+ future tenants). Mirrors the wider Lux e2e env routing convention:

  E2E_ENV=local (default) → http://localhost:3001 (auto-boots vite preview)
  E2E_ENV=dev             → https://bridge.lux-dev.network
  E2E_ENV=test            → https://bridge.lux-test.network
  E2E_ENV=main            → https://bridge.lux.network
  BRIDGE_TENANT=zoo       → https://bridge.zoo.network (mainnet only)

smoke.spec.ts (6 cases, ~16s):
  1. Renders, title matches brand, SVG brand logo from @luxfi/logo
  2. From-chain selector has ≥ 2 options
  3. From-asset selector has ≥ 2 options after switching to Ethereum
     (default chain Lux only has the native LUX asset)
  4. Connect-wallet button visible + clickable (no real wallet connect)
  5. Typing an amount updates the "You receive" estimate via useSwap
  6. Production gate: served HTML + JS bundles free of `DEADBEEF`
     (the dev-only stub MPC display address)

multi-tenant.spec.ts (skips unless E2E_ENV=main):
  - For each tenant in the registry, probe DNS, navigate, assert
    document.title and the --brand-primary CSS variable set by
    applyBrandMetadata reflect the tenant's pinned brand block.

Playwright config auto-boots `vite preview` on :3001 from app/bridge/
when E2E_ENV=local, so `pnpm test:smoke` is one-step after `pnpm build`.
README documents the build prereq and the runner contract.

Out of scope (TODOs in code):
  - Real wallet connect (Playwright wallet mocking is a separate PR)
  - Real backend swap submit (prod backend not reliably reachable from CI)
  - Visual regression / screenshot diffs (separate PR)
2026-05-21 17:00:58 -07:00

95 lines
3.5 KiB
TypeScript

// Multi-tenant brand-routing spec.
//
// The @luxfi/bridge SDK is jurisdiction-neutral. Tenants (Lux, Zoo, future
// downstream consumers) supply their own BridgeConfig and the SDK applies
// brand metadata to the host document via `applyBrandMetadata` — which
// sets `document.title`, the favicon, and the `--brand-primary` /
// `--brand-secondary` CSS variables on `<html>`.
//
// This spec asserts the per-tenant contract holds end-to-end against the
// live production surface. It SKIPS itself when:
// - E2E_ENV is not `main` (the only env where Zoo's bridge lives today)
// - The tenant DNS doesn't resolve / serve a 2xx (treat as not-yet-deployed)
//
// When a third tenant ships, add another entry to TENANTS below — no other
// changes required.
import { test, expect, request as pwRequest } from '@playwright/test'
interface TenantSpec {
name: string
url: string
expectedTitle: RegExp
/** Hex color the tenant should pin --brand-primary to. */
expectedPrimary: string
}
const TENANTS: TenantSpec[] = [
{
name: 'lux',
url: 'https://bridge.lux.network',
expectedTitle: /lux bridge/i,
// app/bridge/src/bridge.config.ts → brand.primaryColor
expectedPrimary: '#5b8def',
},
{
name: 'zoo',
url: 'https://bridge.zoo.network',
expectedTitle: /zoo bridge/i,
// Zoo's brand yellow (from the user task).
expectedPrimary: '#fcf006',
},
]
const env = process.env.E2E_ENV ?? 'local'
test.describe('multi-tenant brand routing', () => {
test.skip(
env !== 'main',
'Multi-tenant assertions only run against E2E_ENV=main (the surface where every tenant has a public deployment).'
)
for (const tenant of TENANTS) {
test(`[${tenant.name}] ${tenant.url} pins brand primary to ${tenant.expectedPrimary}`, async ({
browser,
}) => {
// Probe DNS / availability before mounting a full page context — saves
// 15s of nav timeout on tenants that haven't shipped yet.
const probe = await pwRequest.newContext({ ignoreHTTPSErrors: false })
const head = await probe.get(tenant.url, { failOnStatusCode: false }).catch(() => null)
await probe.dispose()
test.skip(
!head || head.status() >= 400,
`${tenant.url} not reachable (HEAD failed or non-2xx) — tenant not deployed yet.`
)
const ctx = await browser.newContext()
const page = await ctx.newPage()
const response = await page.goto(tenant.url)
expect(response?.status(), `${tenant.url} responds 2xx`).toBeLessThan(400)
await expect(page).toHaveTitle(tenant.expectedTitle)
// The SDK writes brand.primaryColor onto `--brand-primary` via
// applyBrandMetadata (see pkg/bridge/src/config.ts). Read it back
// from computed style on <html>.
const primary = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--brand-primary').trim()
)
expect(
primary.toLowerCase(),
`${tenant.name} --brand-primary on <html>`
).toBe(tenant.expectedPrimary.toLowerCase())
await ctx.close()
})
}
// TODO: hostname-based brand routing on a SHARED build (i.e. one Docker
// image serves multiple tenants based on the Host header). Today each
// tenant ships its own image (app/bridge for Lux, equivalent dir for Zoo
// when it lands), so the brand block is build-time, not request-time.
// When the shared-image route lands, add a spec that hits the same image
// with `Host: bridge.zoo.network` and verifies Zoo branding.
})