forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgen-ui-interrupt.spec.ts
More file actions
135 lines (117 loc) · 4.96 KB
/
Copy pathgen-ui-interrupt.spec.ts
File metadata and controls
135 lines (117 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { test, expect } from "@playwright/test";
// QA reference: qa/gen-ui-interrupt.md
// Demo source: src/app/demos/gen-ui-interrupt/{page.tsx, time-picker-card.tsx}
//
// Uses `useInterrupt({ renderInChat: true })` — the low-level CopilotKit
// primitive wired to LangGraph's `interrupt()` on the `interrupt_agent`
// graph (shared with `interrupt-headless`). When the agent invokes the
// backend `schedule_meeting` tool, the graph interrupts and a
// `TimePickerCard` renders INLINE in the chat transcript (no portal).
//
// Card states (mutually exclusive, per-interrupt):
// - `time-picker-card` — initial, 4 slot buttons + "None of these work"
// - `time-picker-picked` — after a slot is clicked
// - `time-picker-cancelled` — after the ghost cancel button
//
// Typed prompts (not suggestion pills) are used for the tool-trigger flows:
// pill-click was observed to not always drive schedule_meeting on Railway.
// No LLM-text assertions — only testid state transitions plus the inline
// (non-body) render contract.
test.describe("Gen UI via useInterrupt (inline time picker)", () => {
test.setTimeout(120_000);
test.beforeEach(async ({ page }) => {
// Wait for the CopilotKit runtime info response to complete before
// interacting. Without this, messages sent before the runtime
// connects are silently dropped because the agent reference used by
// CopilotChat's submit handler is a provisional stub whose
// onMessagesChanged subscribers are replaced when the real agent
// arrives — orphaning the in-flight run's state updates.
const runtimeReady = page.waitForResponse(
(res) =>
res.url().includes("/api/copilotkit") &&
res.request().method() === "POST" &&
res.status() === 200,
);
await page.goto("/demos/gen-ui-interrupt");
await runtimeReady;
});
test("page loads with chat input and no picker rendered", async ({
page,
}) => {
await expect(page.getByPlaceholder("Type a message")).toBeVisible();
await expect(page.locator('[data-testid="time-picker-card"]')).toHaveCount(
0,
);
});
test("both suggestion pills render", async ({ page }) => {
const suggestions = page.locator('[data-testid="copilot-suggestion"]');
await expect(
suggestions.filter({ hasText: "Book a call with sales" }).first(),
).toBeVisible({ timeout: 15_000 });
await expect(
suggestions.filter({ hasText: "Schedule a 1:1 with Alice" }).first(),
).toBeVisible({ timeout: 15_000 });
});
test("picking a slot transitions the card to the picked state", async ({
page,
}) => {
const input = page.getByPlaceholder("Type a message");
await input.fill(
"Use schedule_meeting to book an intro call with the sales team about pricing.",
);
await page.locator('[data-testid="copilot-send-button"]').first().click();
const card = page.locator('[data-testid="time-picker-card"]').first();
await expect(card).toBeVisible({ timeout: 60_000 });
// Contract: inline render, NOT a body portal (unlike hitl-in-app).
await expect(
page.locator('body > [data-testid="time-picker-card"]'),
).toHaveCount(0);
const expectedSlots = [
"Tomorrow 10:00 AM",
"Tomorrow 2:00 PM",
"Monday 9:00 AM",
"Monday 3:30 PM",
];
for (const label of expectedSlots) {
await expect(card.getByRole("button", { name: label })).toBeVisible();
}
await expect(
card.getByRole("button", { name: "None of these work" }),
).toBeVisible();
await card.getByRole("button", { name: "Monday 9:00 AM" }).click();
const picked = page.locator('[data-testid="time-picker-picked"]').first();
await expect(picked).toBeVisible({ timeout: 10_000 });
await expect(picked).toContainText("Monday 9:00 AM");
// The picked-state card replaces the interactive card.
await expect(page.locator('[data-testid="time-picker-card"]')).toHaveCount(
0,
);
await expect(
page.locator('[data-testid="copilot-assistant-message"]').first(),
).toBeVisible({
timeout: 45_000,
});
});
test("cancel path: None-of-these-work transitions to cancelled state", async ({
page,
}) => {
const input = page.getByPlaceholder("Type a message");
await input.fill(
"Use schedule_meeting to book a 1:1 with Alice next week to review Q2 goals.",
);
await page.locator('[data-testid="copilot-send-button"]').first().click();
const card = page.locator('[data-testid="time-picker-card"]').first();
await expect(card).toBeVisible({ timeout: 60_000 });
await card.getByRole("button", { name: "None of these work" }).click();
const cancelled = page
.locator('[data-testid="time-picker-cancelled"]')
.first();
await expect(cancelled).toBeVisible({ timeout: 10_000 });
await expect(cancelled).toContainText("Cancelled");
await expect(
page.locator('[data-testid="copilot-assistant-message"]').first(),
).toBeVisible({
timeout: 45_000,
});
});
});