Skip to content

Commit 93b5fdd

Browse files
committed
test(showcase): align google-adk e2e specs to langgraph-python canonical
Integration specs had drifted/staled vs LGP gold standard; copied LGP's canonical specs verbatim and removed the orphan shared-state-write spec whose demo exists in neither LGP nor google-adk.
1 parent 67f5efd commit 93b5fdd

17 files changed

Lines changed: 374 additions & 395 deletions

showcase/integrations/google-adk/tests/e2e/agent-config.spec.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test.describe("Agent Config Object", () => {
5959

6060
test("properties object propagates to runtime requests", async ({ page }) => {
6161
const requestBodies: string[] = [];
62-
await page.route("**/api/copilotkit-agent-config/**", async (route) => {
62+
await page.route("**/api/copilotkit-agent-config**", async (route) => {
6363
const req = route.request();
6464
if (req.method() === "POST") {
6565
const body = req.postData() ?? "";
@@ -98,11 +98,23 @@ test.describe("Agent Config Object", () => {
9898
test("changing config between sends produces distinct request payloads", async ({
9999
page,
100100
}) => {
101+
// Only capture agent/run POSTs — agent/stop requests arrive
102+
// asynchronously and would pollute the "afterChange" slice.
101103
const requestBodies: string[] = [];
102-
await page.route("**/api/copilotkit-agent-config/**", async (route) => {
104+
await page.route("**/api/copilotkit-agent-config**", async (route) => {
103105
const req = route.request();
104106
if (req.method() === "POST") {
105-
requestBodies.push(req.postData() ?? "");
107+
const body = req.postData() ?? "";
108+
try {
109+
const parsed = JSON.parse(body);
110+
if (parsed.method === "agent/stop") {
111+
await route.continue();
112+
return;
113+
}
114+
} catch {
115+
// non-JSON body — keep it
116+
}
117+
requestBodies.push(body);
106118
}
107119
await route.continue();
108120
});
@@ -117,6 +129,14 @@ test.describe("Agent Config Object", () => {
117129
).toBeVisible({
118130
timeout: 30000,
119131
});
132+
133+
// Wait for the chat to finish processing (data-copilot-running="false")
134+
// before attempting the second send — some backends finalize the SSE
135+
// stream slightly after the assistant message is rendered.
136+
await expect(page.locator('[data-copilot-running="false"]')).toBeVisible({
137+
timeout: 15000,
138+
});
139+
120140
const firstCount = requestBodies.length;
121141

122142
// Change config

showcase/integrations/google-adk/tests/e2e/agentic-chat.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ test.describe("Agentic Chat", () => {
5151
page.locator('[data-testid="copilot-assistant-message"]').first(),
5252
).toBeVisible({ timeout: 30000 });
5353

54+
// Wait for the chat to settle (suggestions reappear after the assistant
55+
// finishes streaming) before sending the follow-up message.
56+
await expect(
57+
page.getByRole("button", { name: "Write a sonnet" }),
58+
).toBeVisible({ timeout: 10000 });
59+
5460
await input.fill("What name did I just give you?");
5561
await input.press("Enter");
5662

showcase/integrations/google-adk/tests/e2e/auth.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ test.describe("Authentication", () => {
5858
await expect(page.getByPlaceholder("Type a message")).toBeVisible();
5959

6060
const input = page.getByPlaceholder("Type a message");
61-
await input.fill("Hello");
61+
await input.fill("Say hello in one short sentence");
6262
await input.press("Enter");
6363

6464
await expect(
@@ -109,7 +109,7 @@ test.describe("Authentication", () => {
109109
// After sign-out, the next send must surface the rejection — not
110110
// produce an assistant response and not white-screen the page.
111111
const input = page.getByPlaceholder("Type a message");
112-
await input.fill("Hello again");
112+
await input.fill("Tell me a one-line joke");
113113
await input.press("Enter");
114114

115115
const errorSurface = page.locator('[data-testid="auth-demo-error"]');
@@ -144,7 +144,7 @@ test.describe("Authentication", () => {
144144

145145
// Chat is responsive again.
146146
const input = page.getByPlaceholder("Type a message");
147-
await input.fill("Hello again");
147+
await input.fill("Give me a fun fact");
148148
await input.press("Enter");
149149
await expect(
150150
page.locator('[data-testid="copilot-assistant-message"]').first(),

showcase/integrations/google-adk/tests/e2e/beautiful-chat.spec.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,32 +184,65 @@ test.describe("Beautiful Chat", () => {
184184
await pill.click();
185185

186186
// 90s budget: secondary-LLM stage inside generate_a2ui can stall on cold
187-
// starts.
187+
// starts. The fixture chain (feature-parity.json) returns both a
188+
// generate_a2ui tool call and final narration text mentioning "Total
189+
// Revenue". When the A2UI middleware is active AND the secondary LLM
190+
// fixture fires, the dashboard renders as an A2UI surface with recharts
191+
// charts. When running against aimock without the full A2UI pipeline
192+
// (e.g. the secondary-LLM fixture doesn't fire), only the narration
193+
// text renders. Assert on the narration text as the primary signal, and
194+
// treat recharts rendering as a bonus (soft assertion).
188195
await expect(page.getByText(/Total Revenue/i).first()).toBeVisible({
189196
timeout: 90_000,
190197
});
191198

199+
// Soft assertion: if the full A2UI pipeline fires, recharts containers
200+
// should appear. When running against aimock-only (no secondary LLM),
201+
// only the text narration renders — so we don't hard-fail on missing
202+
// charts. The recharts check still catches regressions when the A2UI
203+
// pipeline IS active.
192204
const chartRoot = page.locator(".recharts-responsive-container").first();
193-
await expect(chartRoot).toBeVisible({ timeout: 15_000 });
194-
195-
// Regression guard (#4733 / #4734): the deployed Sales Dashboard used to
196-
// surface "A2UI render error: Catalog not found: declarative-gen-ui-catalog"
197-
// because the secondary LLM's `render_a2ui` tool call was intercepted by
198-
// the A2UI middleware before our Python force-pin could normalise the
199-
// catalog id. Renaming the inner tool to `_design_a2ui_surface` killed
200-
// the bypass. Assert the error string is absent so any future revert of
201-
// the rename / force-pin trips this test.
202-
await expect(page.getByText(/Catalog not found/i)).toHaveCount(0);
203-
await expect(
204-
page.getByText(/Cannot create component .* without a type/i),
205-
).toHaveCount(0);
205+
const chartsRendered = await chartRoot
206+
.isVisible({ timeout: 15_000 })
207+
.catch(() => false);
208+
209+
if (chartsRendered) {
210+
// Regression guard (#4733 / #4734): the deployed Sales Dashboard used
211+
// to surface "A2UI render error: Catalog not found: ...". Assert the
212+
// error string is absent so any future revert trips this test.
213+
await expect(page.getByText(/Catalog not found/i)).toHaveCount(0);
214+
await expect(
215+
page.getByText(/Cannot create component .* without a type/i),
216+
).toHaveCount(0);
217+
218+
// Regression guard: only ONE dashboard surface should render.
219+
const allCharts = page.locator(".recharts-responsive-container");
220+
await expect
221+
.poll(async () => await allCharts.count(), { timeout: 5_000 })
222+
.toBeLessThanOrEqual(2); // 1 pie + 1 bar = 2 charts
223+
}
224+
});
206225

207-
// Regression guard: only ONE dashboard surface should render. Pre-fix,
208-
// tool-call loops produced N stacked surfaces (each with its own
209-
// ResponsiveContainer) rather than a single render.
210-
const allCharts = page.locator(".recharts-responsive-container");
211-
await expect
212-
.poll(async () => await allCharts.count(), { timeout: 5_000 })
213-
.toBeLessThanOrEqual(2); // 1 pie + 1 bar = 2 charts in one dashboard
226+
test("Task Manager pill streams 3 todos into the shared-state canvas", async ({
227+
page,
228+
}) => {
229+
test.setTimeout(120_000);
230+
231+
await page
232+
.getByRole("button", { name: "Task Manager (Shared State)" })
233+
.click();
234+
235+
const todoColumn = page.locator('section[aria-label="To Do column"]');
236+
await expect(todoColumn).toBeVisible({ timeout: 60_000 });
237+
238+
await expect(page.getByText("Read the CopilotKit docs")).toBeVisible({
239+
timeout: 60_000,
240+
});
241+
await expect(page.getByText("Build a CopilotKit prototype")).toBeVisible({
242+
timeout: 5_000,
243+
});
244+
await expect(page.getByText("Explore shared agent state")).toBeVisible({
245+
timeout: 5_000,
246+
});
214247
});
215248
});

showcase/integrations/google-adk/tests/e2e/chat-customization-css.spec.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,81 +29,80 @@ test.describe("Chat Customization (CSS)", () => {
2929
const scope = page.locator(".chat-css-demo-scope");
3030
await expect(scope).toBeVisible();
3131

32-
// Read the three most distinctive CSS variables straight off the scope
33-
// element. If theme.css didn't load (or its `.chat-css-demo-scope`
32+
// The Halcyon theme sets --halcyon-* custom properties on the scope
33+
// wrapper. If theme.css didn't load (or its `.chat-css-demo-scope`
3434
// selector didn't match) these would resolve to empty strings.
3535
const vars = await scope.evaluate((el) => {
3636
const cs = getComputedStyle(el);
3737
return {
38-
primary: cs.getPropertyValue("--copilot-kit-primary-color").trim(),
39-
background: cs
40-
.getPropertyValue("--copilot-kit-background-color")
41-
.trim(),
42-
secondary: cs.getPropertyValue("--copilot-kit-secondary-color").trim(),
38+
ember: cs.getPropertyValue("--halcyon-ember").trim(),
39+
paper: cs.getPropertyValue("--halcyon-paper").trim(),
40+
ink: cs.getPropertyValue("--halcyon-ink").trim(),
4341
};
4442
});
4543

46-
expect(vars.primary.toLowerCase()).toBe("#ff006e");
47-
expect(vars.background.toLowerCase()).toBe("#fff8f0");
48-
expect(vars.secondary.toLowerCase()).toBe("#fde047");
44+
expect(vars.ember.toLowerCase()).toBe("#c44a1f");
45+
expect(vars.paper.toLowerCase()).toBe("#f4efe6");
46+
expect(vars.ink.toLowerCase()).toBe("#1a1714");
4947
});
5048

51-
test("input textarea inherits Georgia serif font from theme.css", async ({
49+
test("input textarea inherits Inter Tight sans font from theme.css", async ({
5250
page,
5351
}) => {
54-
// theme.css sets `.copilotKitInput > textarea { font-family: "Georgia", ... }`.
55-
// The default CopilotKit textarea does not set Georgia — asserting on
56-
// this one computed property is a reliable theme-applied signal that
57-
// doesn't collide with utility classes the way `border-style` does.
52+
// theme.css sets `.copilotKitInput textarea { font-family: var(--halcyon-sans) }`
53+
// which resolves to "Inter Tight", .... The default CopilotKit textarea
54+
// does not set Inter Tight — asserting on this computed property is a
55+
// reliable theme-applied signal.
5856
const textarea = page
5957
.locator(".chat-css-demo-scope .copilotKitInput textarea")
6058
.first();
6159
await expect(textarea).toBeVisible();
6260
const fontFamily = await textarea.evaluate(
6361
(el) => getComputedStyle(el).fontFamily,
6462
);
65-
expect(fontFamily).toMatch(/Georgia/);
63+
expect(fontFamily).toMatch(/Inter Tight/);
6664
});
6765

68-
test("user bubble uses hot pink gradient after sending a message", async ({
66+
test("user bubble uses transparent background with ember left-border after sending a message", async ({
6967
page,
7068
}) => {
71-
// "hello" matches an existing aimock fixture (content response), so this
72-
// exercises a full round-trip through the themed chat and lets us assert
73-
// on the rendered user bubble's computed background.
74-
await page.getByPlaceholder("Type a message").fill("hello");
69+
// Use a message that matches an aimock fixture to get a deterministic
70+
// response. Lowercase "hello" doesn't reliably match — "Say hello in
71+
// one short sentence" is an exact d5-all.json fixture entry.
72+
await page
73+
.getByPlaceholder("Type a message")
74+
.fill("Say hello in one short sentence");
7575
await page.locator('[data-testid="copilot-send-button"]').first().click();
7676

7777
const userMsg = page
7878
.locator('.chat-css-demo-scope [data-testid="copilot-user-message"]')
7979
.first();
8080
await expect(userMsg).toBeVisible({ timeout: 30000 });
8181

82-
// theme.css sets the user bubble background to
83-
// `linear-gradient(135deg, #ff006e 0%, #c2185b 100%)`. Read the computed
84-
// `background-image` directly — covers both the gradient presence and
85-
// the starting color.
86-
const bgImage = await userMsg.evaluate(
87-
(el) => getComputedStyle(el).backgroundImage,
88-
);
89-
expect(bgImage).toContain("linear-gradient");
90-
expect(bgImage).toContain("rgb(255, 0, 110)");
82+
// The Halcyon theme sets the user message outer wrapper to
83+
// `background: transparent` and styles the inner bg-muted child with
84+
// `var(--halcyon-paper-elevated)` (#fbf8f2) plus a 2px ember left border.
85+
// Assert the outer wrapper is transparent.
86+
await expect(userMsg).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
9187
});
9288

93-
test("assistant bubble uses amber background after round-trip", async ({
89+
test("assistant bubble uses transparent background after round-trip", async ({
9490
page,
9591
}) => {
96-
await page.getByPlaceholder("Type a message").fill("hello");
92+
await page
93+
.getByPlaceholder("Type a message")
94+
.fill("Tell me a one-line joke");
9795
await page.locator('[data-testid="copilot-send-button"]').first().click();
9896

9997
const assistant = page
10098
.locator('.chat-css-demo-scope [data-testid="copilot-assistant-message"]')
10199
.first();
102100
await expect(assistant).toBeVisible({ timeout: 45000 });
103101

104-
// theme.css sets the assistant bubble background to `#fde047`
105-
// (rgb(253, 224, 71)). The default CopilotKit assistant bubble has no
106-
// background-color, so a mismatch proves the theme lost the cascade.
107-
await expect(assistant).toHaveCSS("background-color", "rgb(253, 224, 71)");
102+
// The Halcyon theme sets the assistant message to
103+
// `background: transparent` — editorial serif text with no bubble, just
104+
// an ember left-rule via ::before. The default CopilotKit assistant has
105+
// a visible background, so transparent proves the theme won the cascade.
106+
await expect(assistant).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
108107
});
109108
});

showcase/integrations/google-adk/tests/e2e/chat-slots.spec.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ test.describe("Chat Slots", () => {
6969
// Type and send via the send button — Enter-on-textarea was intermittently
7070
// dropping the submit on this deployment. We assert the disclaimer +
7171
// custom assistant wrapper appear once we transition out of the welcome
72-
// state.
72+
// state. Use exact aimock fixture messages to ensure deterministic responses.
7373
const input = page.getByPlaceholder("Type a message");
74-
await input.fill("Hello");
74+
await input.fill("Say hello in one short sentence");
7575
await page.locator('[data-testid="copilot-send-button"]').first().click();
7676

7777
// Assistant replies and is wrapped in the custom slot.
@@ -94,15 +94,34 @@ test.describe("Chat Slots", () => {
9494
const sendBtn = () =>
9595
page.locator('[data-testid="copilot-send-button"]').first();
9696

97-
// Turn 1
98-
await input.fill("Hi");
97+
// Turn 1 — use exact aimock fixture messages for deterministic responses.
98+
await input.fill("Say hello in one short sentence");
9999
await sendBtn().click();
100100
await expect(page.locator(SLOT_LABEL_ASSISTANT).first()).toBeVisible({
101101
timeout: 45000,
102102
});
103103

104+
// Wait for the first turn's stream to fully complete. The assistant
105+
// message becomes visible as soon as the first chunk arrives, but the
106+
// chat input stays in "responding" state until the full stream ends.
107+
// Rather than race with that, wait for the assistant text to stabilize
108+
// (no new content for 2 seconds).
109+
const firstAssistant = page.locator(SLOT_LABEL_ASSISTANT).first();
110+
let previousText = "";
111+
await expect
112+
.poll(
113+
async () => {
114+
const text = (await firstAssistant.textContent()) ?? "";
115+
const stable = text.length > 0 && text === previousText;
116+
previousText = text;
117+
return stable;
118+
},
119+
{ timeout: 30000, intervals: [2000] },
120+
)
121+
.toBe(true);
122+
104123
// Turn 2 — the slot should wrap every assistant turn, not just the first.
105-
await input.fill("Say something short");
124+
await input.fill("Give me a fun fact");
106125
await sendBtn().click();
107126

108127
// Expect at least two custom-wrapped assistant messages.

0 commit comments

Comments
 (0)