Skip to content

Commit 2a14974

Browse files
feat(test): add Playwright e2e tests with mock IPC layer
21 tests across sidebar, composer, chat, overlays, and tabs. Mock IPC intercepts all Tauri invoke commands with in-memory state. Includes simulated streaming response for send-and-receive test.
1 parent d0e8317 commit 2a14974

10 files changed

Lines changed: 1036 additions & 3 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ Thumbs.db
2525

2626
# App data
2727
.codeforge/
28+
playwright-report/
29+
test-results/
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { test, expect } from "@playwright/test";
2+
import { injectMockIPC, seedData, gotoApp, selectThread, sendMessageAndWait } from "./helpers";
3+
4+
test.beforeEach(async ({ page }) => {
5+
await injectMockIPC(page);
6+
});
7+
8+
test.describe("Chat area", () => {
9+
test('shows "New conversation" when thread is open with no messages', async ({ page }) => {
10+
await seedData(page, [
11+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Empty Thread" }] },
12+
]);
13+
await gotoApp(page);
14+
await selectThread(page, "Empty Thread");
15+
16+
const empty = page.locator(".chat-empty");
17+
await expect(empty).toBeVisible();
18+
await expect(empty).toContainText("New conversation");
19+
});
20+
21+
test("renders messages for a thread", async ({ page }) => {
22+
await seedData(page, [
23+
{
24+
name: "Proj",
25+
path: "/tmp/p",
26+
threads: [
27+
{
28+
title: "Chat Thread",
29+
messages: [
30+
{ role: "user", content: "What is Rust?" },
31+
{ role: "assistant", content: "Rust is a systems programming language." },
32+
],
33+
},
34+
],
35+
},
36+
]);
37+
await gotoApp(page);
38+
await selectThread(page, "Chat Thread");
39+
40+
// Wait for messages to render
41+
await expect(page.locator(".message")).toHaveCount(2);
42+
});
43+
44+
test("messages have correct role classes", async ({ page }) => {
45+
await seedData(page, [
46+
{
47+
name: "Proj",
48+
path: "/tmp/p",
49+
threads: [
50+
{
51+
title: "Role Thread",
52+
messages: [
53+
{ role: "user", content: "Hello" },
54+
{ role: "assistant", content: "Hi there" },
55+
{ role: "system", content: "System notice" },
56+
],
57+
},
58+
],
59+
},
60+
]);
61+
await gotoApp(page);
62+
await selectThread(page, "Role Thread");
63+
64+
await expect(page.locator(".message")).toHaveCount(3);
65+
await expect(page.locator(".message-user")).toHaveCount(1);
66+
await expect(page.locator(".message-assistant")).toHaveCount(1);
67+
await expect(page.locator(".message-system")).toHaveCount(1);
68+
});
69+
70+
test("sending a message shows user bubble and streams assistant response", async ({ page }) => {
71+
await seedData(page, [
72+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Send Test" }] },
73+
]);
74+
await gotoApp(page);
75+
await selectThread(page, "Send Test");
76+
77+
// Send a message — the mock IPC will emit streaming events
78+
await sendMessageAndWait(page, "Hello Claude!");
79+
80+
// User message should appear
81+
const userMsg = page.locator(".message-user");
82+
await expect(userMsg).toHaveCount(1);
83+
await expect(userMsg).toContainText("Hello Claude!");
84+
85+
// Assistant response should appear (streamed via mock agent events)
86+
const assistantMsg = page.locator(".message-assistant");
87+
await expect(assistantMsg).toHaveCount(1);
88+
// The mock responds with: 'This is a mock response to: "Hello Claude!"'
89+
await expect(assistantMsg).toContainText("mock response");
90+
91+
// Streaming should be done — no cursor visible
92+
await expect(page.locator(".cursor")).toHaveCount(0);
93+
});
94+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { test, expect } from "@playwright/test";
2+
import { injectMockIPC, seedData, gotoApp, selectThread, typeInComposer } from "./helpers";
3+
4+
test.beforeEach(async ({ page }) => {
5+
await injectMockIPC(page);
6+
});
7+
8+
test.describe("Composer", () => {
9+
test("shows when a thread is active", async ({ page }) => {
10+
await seedData(page, [
11+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Active Thread" }] },
12+
]);
13+
await gotoApp(page);
14+
15+
// Composer should not be visible when no thread is selected
16+
await expect(page.locator(".composer-wrapper")).toHaveCount(0);
17+
18+
// Select the thread
19+
await selectThread(page, "Active Thread");
20+
21+
// Now the composer should appear
22+
await expect(page.locator(".composer-wrapper")).toBeVisible();
23+
});
24+
25+
test("can type in composer textarea", async ({ page }) => {
26+
await seedData(page, [
27+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Typing Test" }] },
28+
]);
29+
await gotoApp(page);
30+
await selectThread(page, "Typing Test");
31+
32+
await typeInComposer(page, "Hello, world!");
33+
34+
const input = page.locator(".composer-input");
35+
await expect(input).toHaveValue("Hello, world!");
36+
});
37+
38+
test("send button is visible", async ({ page }) => {
39+
await seedData(page, [
40+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Send Test" }] },
41+
]);
42+
await gotoApp(page);
43+
await selectThread(page, "Send Test");
44+
45+
const sendBtn = page.locator(".send-btn");
46+
await expect(sendBtn).toBeVisible();
47+
});
48+
49+
test("provider picker button works", async ({ page }) => {
50+
await seedData(page, [
51+
{ name: "Proj", path: "/tmp/p", threads: [{ title: "Provider Test" }] },
52+
]);
53+
await gotoApp(page);
54+
await selectThread(page, "Provider Test");
55+
56+
// The provider pill is the first .meta-pill
57+
const providerPill = page.locator(".meta-pill").first();
58+
await expect(providerPill).toBeVisible();
59+
await expect(providerPill).toContainText("Claude Code");
60+
61+
// Click it to open the provider picker
62+
await providerPill.click();
63+
64+
// The provider picker overlay should now be visible
65+
// (ProviderPicker component renders when providerPickerOpen is true)
66+
// We just verify the click didn't error and the pill is still there
67+
await expect(providerPill).toBeVisible();
68+
});
69+
});

0 commit comments

Comments
 (0)