Skip to content

Commit 8d39b53

Browse files
tylerslatonclaude
andcommitted
fix(showcase): rework auth probe and e2e for unmount-based sign-out flow
The auth demo refactor flipped its lifecycle: unauthenticated is now the default state, <CopilotKit> only mounts after sign-in, and sign-out unmounts the entire chat tree (instead of leaving stale auth headers in a still-mounted chat). The old probe + e2e spec chased a 401-error-banner surface that no longer exists in the new demo, plus a brittle 500ms hardcoded `useEffect` flush wait. Probe rewrite (`d5-auth.ts` + tests): - Add `buildAuthPreFill` that clicks the SignInCard's sign-in button before turn 1, then waits for the chat textarea to mount (proves <CopilotKit> handshook with the runtime). - `buildAuthAssertion` now clicks sign-out, then waits for SignInCard to re-mount. The unmount marker IS the proof — no chat-send-and-401 dance is needed (or possible — there's no chat to send into). - Drop the hardcoded 500ms setTimeout, the unauth-banner wait, and the error-surface poll. None apply to the new flow. E2E rewrite (`tests/e2e/auth.spec.ts`): - "page loads unauthenticated with SignInCard visible" - "signing in mounts the chat surface with AuthBanner" - "authenticated send produces an assistant response" - "signing out unmounts the chat tree and re-renders SignInCard" - "signing back in re-mounts a fresh chat surface" Fixture comment updated to reflect the new flow. The user message ("auth check turn 1") and content response are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4f26734 commit 8d39b53

4 files changed

Lines changed: 235 additions & 203 deletions

File tree

showcase/harness/fixtures/d5/auth.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"_comment": "D5 fixture for /demos/auth. The script signs in by default (the demo page hydrates authenticated), sends one message, then in the assertion clicks [data-testid='auth-sign-out-button'] and waits for [data-testid='auth-demo-error'] OR [data-testid='auth-demo-chat-boundary'] to surface — proves the auth gate actually rejects post-sign-out activity. Substring 'auth check turn 1' is unique.",
2+
"_comment": "D5 fixture for /demos/auth. The demo defaults to UNAUTHENTICATED — the probe's preFill clicks the SignInCard's sign-in button to mount <CopilotKit> + chat, the runner sends 'auth check turn 1', the assertion clicks sign-out and waits for the SignInCard to re-mount (proves the entire chat tree unmounted on sign-out). Substring 'auth check turn 1' is unique across the d5-all bundle.",
33
"fixtures": [
44
{
55
"match": { "userMessage": "auth check turn 1" },

showcase/harness/src/probes/scripts/d5-auth.test.ts

Lines changed: 96 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,75 @@ import type { Page } from "../helpers/conversation-runner.js";
55
import {
66
buildTurns,
77
buildAuthAssertion,
8+
buildAuthPreFill,
9+
SIGN_IN_BUTTON_SELECTOR,
10+
SIGN_IN_CARD_SELECTOR,
811
SIGN_OUT_BUTTON_SELECTOR,
9-
AUTH_BANNER_UNAUTHENTICATED_SELECTOR,
10-
ERROR_BANNER_SELECTOR,
11-
ERROR_BOUNDARY_SELECTOR,
1212
} from "./d5-auth.js";
1313

1414
interface FakeOpts {
15+
/** Whether the sign-in button is initially visible. Default true. */
16+
signInButtonVisible?: boolean;
17+
/** Whether the chat textarea mounts after sign-in. Default true. */
18+
textareaMountsAfterSignIn?: boolean;
19+
/** Whether the sign-out button is visible after authentication. Default true. */
1520
signOutButtonVisible?: boolean;
16-
/** Whether the banner flips to unauthenticated after sign-out. Default true. */
17-
bannerFlipsToUnauth?: boolean;
18-
errorAfterClick?: boolean;
21+
/** Whether the SignInCard re-mounts after sign-out. Default true. */
22+
signInCardRemounts?: boolean;
1923
}
2024

2125
function makePage(opts: FakeOpts): {
2226
page: Page;
23-
/** Inject as the `click` option to `buildAuthAssertion` so the fake
24-
* page's `clicked` flag flips when the assertion tries to sign out. */
2527
fakeClick: (p: Page, sel: string) => Promise<void>;
2628
} {
27-
let clicked = false;
29+
let signedIn = false;
30+
let signedOut = false;
2831
const page: Page = {
2932
async waitForSelector(selector: string) {
30-
// Sign-out button selector — gates on signOutButtonVisible
33+
if (selector === SIGN_IN_BUTTON_SELECTOR) {
34+
if (!(opts.signInButtonVisible ?? true)) {
35+
throw new Error("waitForSelector timeout (sign-in button missing)");
36+
}
37+
return;
38+
}
39+
// The chat-input cascade selector used by the preFill hook.
40+
if (selector.includes("copilot-chat-textarea") || selector === "textarea") {
41+
if (!signedIn) {
42+
throw new Error(
43+
"waitForSelector timeout (chat textarea not mounted before sign-in)",
44+
);
45+
}
46+
if (!(opts.textareaMountsAfterSignIn ?? true)) {
47+
throw new Error(
48+
"waitForSelector timeout (chat textarea did not mount)",
49+
);
50+
}
51+
return;
52+
}
3153
if (selector === SIGN_OUT_BUTTON_SELECTOR) {
32-
if (!opts.signOutButtonVisible) {
33-
throw new Error("waitForSelector timeout (test fake)");
54+
if (!(opts.signOutButtonVisible ?? true)) {
55+
throw new Error("waitForSelector timeout (sign-out button missing)");
3456
}
3557
return;
3658
}
37-
// Auth banner unauthenticated selector — gates on bannerFlipsToUnauth
38-
if (selector === AUTH_BANNER_UNAUTHENTICATED_SELECTOR) {
39-
if (!(opts.bannerFlipsToUnauth ?? true)) {
40-
throw new Error("waitForSelector timeout (banner never flipped)");
59+
if (selector === SIGN_IN_CARD_SELECTOR) {
60+
if (!signedOut || !(opts.signInCardRemounts ?? true)) {
61+
throw new Error(
62+
"waitForSelector timeout (SignInCard did not re-mount)",
63+
);
4164
}
4265
return;
4366
}
4467
},
4568
async fill() {},
4669
async press() {},
4770
async evaluate() {
48-
// probeErrorSurfaceVisible polls this — returns true when the
49-
// error surface should be visible (after click + opts say yes).
50-
return (clicked && (opts.errorAfterClick ?? true)) as never;
71+
return undefined as never;
5172
},
5273
};
53-
const fakeClick = async (_p: Page, _sel: string): Promise<void> => {
54-
clicked = true;
74+
const fakeClick = async (_p: Page, sel: string): Promise<void> => {
75+
if (sel === SIGN_IN_BUTTON_SELECTOR) signedIn = true;
76+
if (sel === SIGN_OUT_BUTTON_SELECTOR) signedOut = true;
5577
};
5678
return { page, fakeClick };
5779
}
@@ -64,71 +86,80 @@ describe("d5-auth script", () => {
6486
expect(script?.fixtureFile).toBe("auth.json");
6587
});
6688

67-
it("buildTurns input matches fixture", () => {
89+
it("buildTurns produces one turn with preFill and assertion", () => {
6890
const ctx: D5BuildContext = {
6991
integrationSlug: "langgraph-python",
7092
featureType: "auth",
7193
baseUrl: "https://x.test",
7294
};
73-
expect(buildTurns(ctx)[0]!.input).toBe("auth check turn 1");
95+
const turns = buildTurns(ctx);
96+
expect(turns).toHaveLength(1);
97+
expect(turns[0]!.input).toBe("auth check turn 1");
98+
expect(typeof turns[0]!.preFill).toBe("function");
99+
expect(typeof turns[0]!.assertions).toBe("function");
74100
});
75101

76-
it("exposes the sign-out, banner, and error selectors", () => {
102+
it("exposes the sign-in, sign-out, and SignInCard selectors", () => {
103+
expect(SIGN_IN_BUTTON_SELECTOR).toBe(
104+
'[data-testid="auth-sign-in-button"]',
105+
);
106+
expect(SIGN_IN_CARD_SELECTOR).toBe('[data-testid="auth-sign-in-card"]');
77107
expect(SIGN_OUT_BUTTON_SELECTOR).toBe(
78108
'[data-testid="auth-sign-out-button"]',
79109
);
80-
expect(AUTH_BANNER_UNAUTHENTICATED_SELECTOR).toBe(
81-
'[data-testid="auth-banner"][data-authenticated="false"]',
82-
);
83-
expect(ERROR_BANNER_SELECTOR).toBe('[data-testid="auth-demo-error"]');
84-
expect(ERROR_BOUNDARY_SELECTOR).toBe(
85-
'[data-testid="auth-demo-chat-boundary"]',
86-
);
87110
});
88111

89-
it("assertion fails when sign-out button is not visible", async () => {
90-
const { page, fakeClick } = makePage({ signOutButtonVisible: false });
91-
const assertion = buildAuthAssertion({
92-
signOutTimeoutMs: 50,
93-
click: fakeClick,
112+
describe("buildAuthPreFill", () => {
113+
it("fails when sign-in button is not visible (demo loaded into wrong state)", async () => {
114+
const { page, fakeClick } = makePage({ signInButtonVisible: false });
115+
const preFill = buildAuthPreFill({ click: fakeClick });
116+
await expect(preFill(page)).rejects.toThrow(
117+
/sign-in button.*not visible/,
118+
);
94119
});
95-
await expect(assertion(page)).rejects.toThrow(
96-
/sign-out button.*not visible/,
97-
);
98-
});
99120

100-
it("assertion fails when banner does not flip to unauthenticated after sign-out", async () => {
101-
const { page, fakeClick } = makePage({
102-
signOutButtonVisible: true,
103-
bannerFlipsToUnauth: false,
121+
it("fails when chat textarea does not mount after sign-in", async () => {
122+
const { page, fakeClick } = makePage({
123+
textareaMountsAfterSignIn: false,
124+
});
125+
const preFill = buildAuthPreFill({ click: fakeClick });
126+
await expect(preFill(page)).rejects.toThrow(
127+
/chat textarea did not mount/,
128+
);
104129
});
105-
const assertion = buildAuthAssertion({
106-
signOutTimeoutMs: 50,
107-
click: fakeClick,
130+
131+
it("succeeds when sign-in mounts the chat surface", async () => {
132+
const { page, fakeClick } = makePage({});
133+
const preFill = buildAuthPreFill({ click: fakeClick });
134+
await expect(preFill(page)).resolves.toBeUndefined();
108135
});
109-
await expect(assertion(page)).rejects.toThrow(
110-
/banner did not flip to unauthenticated/,
111-
);
112136
});
113137

114-
it("assertion fails when error surfaces never appear after sign-out", async () => {
115-
const { page, fakeClick } = makePage({
116-
signOutButtonVisible: true,
117-
errorAfterClick: false,
138+
describe("buildAuthAssertion", () => {
139+
it("fails when sign-out button is not visible (sign-in path didn't authenticate)", async () => {
140+
const { page, fakeClick } = makePage({ signOutButtonVisible: false });
141+
const assertion = buildAuthAssertion({
142+
signOutTimeoutMs: 50,
143+
click: fakeClick,
144+
});
145+
await expect(assertion(page)).rejects.toThrow(
146+
/sign-out button.*not visible/,
147+
);
118148
});
119-
const assertion = buildAuthAssertion({
120-
signOutTimeoutMs: 50,
121-
click: fakeClick,
149+
150+
it("fails when SignInCard does not re-mount after sign-out (tree didn't unmount)", async () => {
151+
const { page, fakeClick } = makePage({ signInCardRemounts: false });
152+
const assertion = buildAuthAssertion({
153+
signOutTimeoutMs: 50,
154+
click: fakeClick,
155+
});
156+
await expect(assertion(page)).rejects.toThrow(/SignInCard.*did not re-mount/);
122157
});
123-
await expect(assertion(page)).rejects.toThrow(/neither.*appeared/);
124-
});
125158

126-
it("assertion succeeds when error banner appears after sign-out", async () => {
127-
const { page, fakeClick } = makePage({
128-
signOutButtonVisible: true,
129-
errorAfterClick: true,
159+
it("succeeds when SignInCard re-mounts after clicking sign-out", async () => {
160+
const { page, fakeClick } = makePage({});
161+
const assertion = buildAuthAssertion({ click: fakeClick });
162+
await expect(assertion(page)).resolves.toBeUndefined();
130163
});
131-
const assertion = buildAuthAssertion({ click: fakeClick });
132-
await expect(assertion(page)).resolves.toBeUndefined();
133164
});
134165
});

0 commit comments

Comments
 (0)