Skip to content

Commit 1f74d72

Browse files
committed
test(e2e): add custom-auth-server HTTP smoke on :4001
Wire non-browser boot smoke for the Express example (AD-6): ui|http example kind, serial runner entry, and spec-managed lifecycle on :4001. Update testing-strategy and AD-6 as durable owners; defer Phase 5 auth flags (no flakiness observed).
1 parent b8526a9 commit 1f74d72

9 files changed

Lines changed: 224 additions & 91 deletions

File tree

developer-docs/architecture/testing-strategy.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ type: Reference
33
title: Testing strategy
44
description: Verification layers for firebaseui-web packages and examples.
55
tags: [testing, ci, playwright]
6-
timestamp: 2026-07-01T00:00:00Z
6+
timestamp: 2026-07-03T00:00:00Z
77
---
88

99
# Layers
@@ -13,18 +13,19 @@ timestamp: 2026-07-01T00:00:00Z
1313
| Package unit tests | `pnpm test` | [CONTRIBUTING.md](../../CONTRIBUTING.md) | Core/react/angular/shadcn/translations/styles logic |
1414
| Compile (packages + examples) | `pnpm build` | [CONTRIBUTING.md](../../CONTRIBUTING.md) | TS, bundler, framework build errors |
1515
| Lint / format | `pnpm lint:check`, `pnpm format:check` | [CONTRIBUTING.md](../../CONTRIBUTING.md) | Style regressions |
16-
| Example smoke (browser + HTTP) | `pnpm test:e2e` | [work-queues/playwright-e2e-smoke.md](../work-queues/playwright-e2e-smoke.md) | Example dev-server load + basic UI interactivity (five UI examples) and `custom-auth-server` HTTP boot |
16+
| Example smoke (browser + HTTP) | `pnpm test:e2e` | [AD-3](../decisions.md#ad-3-playwright-example-smoke-tests-mvp-scope-dev-server), [AD-4](../decisions.md#ad-4-playwright-managed-dev-servers-serial-shared-emulator), [AD-6](../decisions.md#ad-6-custom-auth-server-binds-4001-for-e2e) | Dev-server UI interactivity (five UI examples) and `custom-auth-server` HTTP boot |
1717
| Security audit | `pnpm audit` | [playbooks/dependency-update-verification.md](../playbooks/dependency-update-verification.md) | Known CVEs in lockfile |
1818

1919
# CI today
2020

21-
[`.github/workflows/test.yaml`](../../.github/workflows/test.yaml): `pnpm build`, Auth emulator start, `pnpm test`. **No browser smoke yet** — target of Playwright work queue. e2e will land as a **separate workflow** with broad path triggers — [AD-7](../decisions.md#ad-7-e2e-runs-in-a-separate-ci-workflow-with-broad-triggers).
21+
[`.github/workflows/test.yaml`](../../.github/workflows/test.yaml): `pnpm build`, Auth emulator start, `pnpm test`.
22+
23+
[`.github/workflows/e2e.yaml`](../../.github/workflows/e2e.yaml): `pnpm test:e2e` (six examples), Auth emulator, Playwright Chromium — [AD-7](../decisions.md#ad-7-e2e-runs-in-a-separate-ci-workflow-with-broad-triggers).
2224

2325
# Gaps
2426

25-
* No example-level automated tests yet (Angular example `vitest` has no spec files) — closed by the Playwright work queue.
2627
* Skipped package integration tests (`describe.skip`).
27-
* `custom-auth-server` not built by root `build` (e2e builds it on demand).
28+
* `custom-auth-server` not built by root `pnpm build` (e2e builds it on demand).
2829
* Production build artifacts not exercised by e2e — deferred, [AD-8](../decisions.md#ad-8-production-artifact-e2e-deferred-future-phase).
2930
* `pnpm audit` not gated in CI.
3031

developer-docs/decisions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ Each example has a **unique e2e port** (react 5173, shadcn 5174, nextjs 3000, ne
4242

4343
## AD-6: custom-auth-server binds :4001 for e2e
4444

45-
The Express `custom-auth-server` defaults to `:4000`, which collides with the **Firebase Emulator UI** (`:4000`). For e2e it binds **`:4001`** (via its `PORT` env) so it never conflicts with emulator services. Its smoke is a **non-browser boot + HTTP (`fetch`/`curl`) assertion** — start the built server, assert it responds, tear down — not a Playwright browser flow. This brings every monorepo example (including `custom-auth-server`) under automated e2e coverage.
45+
The Express `custom-auth-server` defaults to `:4000`, which collides with the **Firebase Emulator UI** (`:4000`). For e2e it binds **`:4001`** (via its `PORT` env) so it never conflicts with emulator services. Its smoke is a **non-browser boot + HTTP assertion** — build the example, start the built server, assert it responds, tear down — not a Playwright browser flow. This brings every monorepo example (including `custom-auth-server`) under automated e2e coverage.
46+
47+
**Lifecycle:** Playwright's `webServer` readiness poll accepts only HTTP status `< 400`. Without credentials, `GET /auth/snapchat/config` returns `500`, so the HTTP smoke spec manages build/boot/poll/teardown itself (detached process group, `:4001` via `PORT`). UI examples continue to use Playwright `webServer` as in [AD-4](#ad-4-playwright-managed-dev-servers-serial-shared-emulator).
4648

4749
---
4850

developer-docs/work-queues/playwright-e2e-smoke.md

Lines changed: 38 additions & 66 deletions
Large diffs are not rendered by default.

e2e/fixtures/example-meta.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,49 @@
1414
* limitations under the License.
1515
*/
1616

17-
/** Per-example routing and server metadata for Playwright smoke tests. */
18-
export type ExampleMeta = {
17+
/**
18+
* Whether an example is exercised through the browser (UI smoke S1–S3) or as a
19+
* non-browser HTTP boot smoke (custom-auth-server). See AD-6.
20+
*/
21+
export type ExampleKind = "ui" | "http";
22+
23+
/** Shared server/startup metadata for every Playwright project. */
24+
type BaseExampleMeta = {
1925
/** Playwright project name (matches pnpm filter where applicable). */
2026
name: string;
21-
/** Dev server origin used as Playwright baseURL. */
27+
/** Server origin used as Playwright baseURL. */
2228
baseURL: string;
29+
/** Optional server command for Playwright webServer. */
30+
webServerCommand?: string;
31+
/** Optional Playwright webServer startup timeout (ms). */
32+
webServerTimeoutMs?: number;
33+
/** URL Playwright polls until the server is ready (defaults to baseURL). */
34+
webServerHealthURL?: string;
35+
};
36+
37+
/** Browser example: rendered UI smoke over the sign-in-with-handlers flow. */
38+
export type UiExampleMeta = BaseExampleMeta & {
39+
kind: "ui";
2340
/** Deep-link path for the sign-in-with-handlers smoke entry screen. */
2441
signInWithHandlersPath: string;
2542
/** Whether URLs for this example require a trailing slash (Next.js). */
2643
trailingSlash: boolean;
2744
/** Forgot-password route from sign-in-with-handlers (S3 asserts UI, not this path). */
2845
forgotPasswordPath: string;
29-
/** Optional dev-server command for Playwright webServer. */
30-
webServerCommand?: string;
31-
/** Optional Playwright webServer startup timeout (ms). */
32-
webServerTimeoutMs?: number;
33-
/** URL Playwright polls until the dev server is ready (defaults to baseURL). */
34-
webServerHealthURL?: string;
3546
};
3647

48+
/** Non-browser example: boot the server and assert an HTTP response. */
49+
export type HttpExampleMeta = BaseExampleMeta & {
50+
kind: "http";
51+
/** Path probed by the HTTP smoke (relative to baseURL). */
52+
smokePath: string;
53+
};
54+
55+
export type ExampleMeta = UiExampleMeta | HttpExampleMeta;
56+
3757
export const exampleMeta: Record<string, ExampleMeta> = {
3858
react: {
59+
kind: "ui",
3960
name: "react",
4061
baseURL: "http://localhost:5173",
4162
signInWithHandlersPath: "/screens/sign-in-auth-screen-w-handlers",
@@ -44,6 +65,7 @@ export const exampleMeta: Record<string, ExampleMeta> = {
4465
webServerCommand: "pnpm --filter=react exec vite --port 5173 --strictPort",
4566
},
4667
shadcn: {
68+
kind: "ui",
4769
name: "shadcn",
4870
baseURL: "http://localhost:5174",
4971
signInWithHandlersPath: "/screens/sign-in-auth-screen-w-handlers",
@@ -52,6 +74,7 @@ export const exampleMeta: Record<string, ExampleMeta> = {
5274
webServerCommand: "pnpm --filter=shadcn exec vite --port 5174 --strictPort",
5375
},
5476
nextjs: {
77+
kind: "ui",
5578
name: "nextjs",
5679
baseURL: "http://localhost:3000",
5780
signInWithHandlersPath: "/screens/sign-in-auth-screen-w-handlers",
@@ -60,6 +83,7 @@ export const exampleMeta: Record<string, ExampleMeta> = {
6083
webServerCommand: "pnpm --filter=nextjs exec next dev --turbopack -p 3000",
6184
},
6285
"nextjs-ssr": {
86+
kind: "ui",
6387
name: "nextjs-ssr",
6488
baseURL: "http://localhost:3001",
6589
signInWithHandlersPath: "/screens/sign-in-auth-screen-w-handlers",
@@ -69,17 +93,30 @@ export const exampleMeta: Record<string, ExampleMeta> = {
6993
webServerTimeoutMs: 180_000,
7094
},
7195
"angular-example": {
96+
kind: "ui",
7297
name: "angular-example",
7398
baseURL: "http://localhost:4200",
7499
signInWithHandlersPath: "/screens/sign-in-auth-screen-w-handlers",
75100
trailingSlash: false,
76101
forgotPasswordPath: "/screens/forgot-password-auth-screen",
77102
webServerCommand: "pnpm --filter=angular-example run start --port 4200",
78103
},
104+
// AD-6: non-browser HTTP smoke; lifecycle in custom-auth-server.spec.ts
105+
"custom-auth-server": {
106+
kind: "http",
107+
name: "custom-auth-server",
108+
baseURL: "http://localhost:4001",
109+
smokePath: "/auth/snapchat/config",
110+
},
79111
};
80112

113+
/** UI examples (browser smoke S1–S3), narrowed by discriminant. */
114+
export const uiExampleEntries: [string, UiExampleMeta][] = Object.entries(exampleMeta).filter(
115+
(entry): entry is [string, UiExampleMeta] => entry[1].kind === "ui"
116+
);
117+
81118
/** Returns the sign-in-with-handlers path respecting per-example trailing-slash rules. */
82-
export function signInWithHandlersUrl(meta: ExampleMeta): string {
119+
export function signInWithHandlersUrl(meta: UiExampleMeta): string {
83120
const { signInWithHandlersPath, trailingSlash } = meta;
84121

85122
if (trailingSlash) {

e2e/playwright.config.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,22 @@ function webServerForMeta(meta: ExampleMeta) {
3939

4040
/** Playwright only reads top-level webServer (not per-project). Serial runner sets E2E_PROJECT. */
4141
function resolveWebServerMeta() {
42-
const fromEnv = process.env.E2E_PROJECT ? exampleMeta[process.env.E2E_PROJECT] : undefined;
43-
44-
if (fromEnv?.webServerCommand) {
45-
return fromEnv;
42+
if (process.env.E2E_PROJECT) {
43+
return exampleMeta[process.env.E2E_PROJECT];
4644
}
4745

4846
return Object.values(exampleMeta).find((meta) => meta.webServerCommand);
4947
}
5048

5149
const webServerMeta = resolveWebServerMeta();
50+
const webServer = webServerMeta ? webServerForMeta(webServerMeta) : undefined;
5251

5352
export default defineConfig({
5453
testDir: "./tests",
5554
workers: 1,
5655
globalSetup: "./global-setup.ts",
5756
globalTeardown: "./global-teardown.ts",
58-
...(webServerMeta ? { webServer: webServerForMeta(webServerMeta) } : {}),
57+
...(webServer ? { webServer } : {}),
5958
projects: Object.values(exampleMeta).map((meta) => ({
6059
name: meta.name,
6160
timeout: meta.name === "angular-example" ? 90_000 : undefined,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
18+
import path from "node:path";
19+
import { fileURLToPath } from "node:url";
20+
import { expect, request, test } from "@playwright/test";
21+
import { exampleMeta, type HttpExampleMeta } from "../fixtures/example-meta";
22+
23+
/** HTTP boot smoke for custom-auth-server — AD-6. */
24+
const PROJECT = "custom-auth-server";
25+
const PORT = "4001";
26+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27+
28+
const meta = exampleMeta[PROJECT];
29+
if (meta.kind !== "http") {
30+
throw new Error(`Expected ${PROJECT} to be an http example, got kind "${meta.kind}"`);
31+
}
32+
const httpMeta: HttpExampleMeta = meta;
33+
34+
let server: ChildProcess | undefined;
35+
36+
function stopServer(): void {
37+
if (!server || server.pid === undefined || server.exitCode !== null) {
38+
return;
39+
}
40+
// Spawned detached so teardown can signal the process group (AD-6).
41+
try {
42+
process.kill(-server.pid, "SIGTERM");
43+
} catch {
44+
server.kill("SIGTERM");
45+
}
46+
}
47+
48+
async function waitForServer(url: string, timeoutMs = 60_000): Promise<void> {
49+
const context = await request.newContext();
50+
const deadline = Date.now() + timeoutMs;
51+
52+
try {
53+
while (Date.now() < deadline) {
54+
if (server?.exitCode != null) {
55+
throw new Error(`custom-auth-server exited early with code ${server.exitCode}`);
56+
}
57+
try {
58+
// Any HTTP response means the server is listening (AD-6).
59+
await context.get(url, { timeout: 2_000 });
60+
return;
61+
} catch {
62+
await new Promise((resolve) => setTimeout(resolve, 500));
63+
}
64+
}
65+
throw new Error(`custom-auth-server did not respond at ${url} within ${timeoutMs}ms`);
66+
} finally {
67+
await context.dispose();
68+
}
69+
}
70+
71+
test.describe(`custom-auth-server HTTP smoke (${PROJECT})`, () => {
72+
test.beforeAll(async ({}, testInfo) => {
73+
test.skip(testInfo.project.name !== PROJECT, `runs only on the ${PROJECT} project`);
74+
test.setTimeout(120_000);
75+
76+
const build = spawnSync("pnpm", ["--filter=custom-auth-server", "run", "build"], {
77+
cwd: REPO_ROOT,
78+
stdio: "inherit",
79+
});
80+
expect(build.status, "custom-auth-server build failed").toBe(0);
81+
82+
server = spawn("pnpm", ["--filter=custom-auth-server", "start"], {
83+
cwd: REPO_ROOT,
84+
stdio: "inherit",
85+
detached: true,
86+
env: { ...process.env, PORT },
87+
});
88+
89+
await waitForServer(`${httpMeta.baseURL}${httpMeta.smokePath}`);
90+
});
91+
92+
test.afterAll(async () => {
93+
stopServer();
94+
});
95+
96+
test("H1: server responds on the config route", async () => {
97+
const context = await request.newContext({ baseURL: httpMeta.baseURL });
98+
99+
try {
100+
const response = await context.get(httpMeta.smokePath);
101+
102+
// 500 without SNAPCHAT_CLIENT_ID (default env); 200 when credentials are present.
103+
expect([200, 500]).toContain(response.status());
104+
105+
const body = await response.json();
106+
107+
if (response.status() === 500) {
108+
expect(body).toMatchObject({ error: "Server missing Snapchat client ID" });
109+
} else {
110+
expect(body).toMatchObject({ clientId: expect.any(String) });
111+
}
112+
} finally {
113+
await context.dispose();
114+
}
115+
});
116+
});

e2e/tests/sign-in-handlers.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { enUs } from "@firebase-oss/ui-translations";
18-
import { exampleMeta, signInWithHandlersUrl } from "../fixtures/example-meta";
18+
import { signInWithHandlersUrl, uiExampleEntries } from "../fixtures/example-meta";
1919
import { expect, test } from "../fixtures/test-harness";
2020

2121
const { labels, errors } = enUs.translations;
@@ -24,7 +24,7 @@ async function waitForSignInForm(page: import("@playwright/test").Page): Promise
2424
await page.waitForSelector('input[name="email"]', { state: "visible", timeout: 30_000 });
2525
}
2626

27-
for (const [projectName, meta] of Object.entries(exampleMeta)) {
27+
for (const [projectName, meta] of uiExampleEntries) {
2828
test.describe(`sign-in with handlers smoke (${projectName})`, () => {
2929
test.beforeEach(({}, testInfo) => {
3030
test.skip(testInfo.project.name !== projectName, `runs only on the ${projectName} project`);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"test:e2e:nextjs": "E2E_PROJECT=nextjs pnpm --filter=e2e exec playwright test --project=nextjs",
1919
"test:e2e:nextjs-ssr": "E2E_PROJECT=nextjs-ssr pnpm --filter=e2e exec playwright test --project=nextjs-ssr",
2020
"test:e2e:angular": "E2E_PROJECT=angular-example pnpm --filter=e2e exec playwright test --project=angular-example",
21+
"test:e2e:custom-auth-server": "E2E_PROJECT=custom-auth-server pnpm --filter=e2e exec playwright test --project=custom-auth-server",
2122
"test:watch": "pnpm --filter=@firebase-oss/ui-core run test:unit:watch & pnpm --filter=@firebase-oss/ui-react run test:unit:watch & pnpm --filter=@firebase-oss/ui-angular run test:watch",
2223
"version:bump:all": "pnpm --filter=@firebase-oss/ui-core run version:bump && pnpm --filter=@firebase-oss/ui-translations run version:bump && pnpm --filter=@firebase-oss/ui-react run version:bump && pnpm --filter=@firebase-oss/ui-styles run version:bump && pnpm --filter=@firebase-oss/ui-angular run version:bump",
2324
"publish:tags:all": "pnpm i && pnpm --filter=@firebase-oss/ui-core run publish:tags && pnpm --filter=@firebase-oss/ui-translations run publish:tags && pnpm --filter=@firebase-oss/ui-react run publish:tags && pnpm --filter=@firebase-oss/ui-styles run publish:tags && pnpm --filter=@firebase-oss/ui-angular run publish:tags",

scripts/e2e-run.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ import { fileURLToPath } from "node:url";
2020

2121
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
2222

23-
/** UI examples exercised serially by `pnpm test:e2e`. */
23+
/** UI examples exercised serially by `pnpm test:e2e` (browser smoke S1–S3). */
2424
const E2E_UI_EXAMPLES = ["react", "shadcn", "nextjs", "nextjs-ssr", "angular-example"];
2525

26+
/** Non-browser examples (HTTP boot smoke), run after the UI examples. AD-6. */
27+
const E2E_HTTP_EXAMPLES = ["custom-auth-server"];
28+
29+
const E2E_EXAMPLES = [...E2E_UI_EXAMPLES, ...E2E_HTTP_EXAMPLES];
30+
2631
function ensurePackagesBuilt() {
2732
console.log("\n[e2e-run] build:packages (once)…\n");
2833
const build = spawnSync("pnpm", ["build:packages"], { cwd: REPO_ROOT, stdio: "inherit" });
@@ -51,7 +56,7 @@ function runExample(example) {
5156

5257
ensurePackagesBuilt();
5358

54-
for (const example of E2E_UI_EXAMPLES) {
59+
for (const example of E2E_EXAMPLES) {
5560
runExample(example);
5661
if (process.exitCode) {
5762
break;

0 commit comments

Comments
 (0)