1
46import type { APIRequestContext } from "@playwright/test";
47import { expect, test } from "../fixtures";
48import { createWorkspace } from "../utils/api";
49
50
51const SECTIONS = ["Objectives", "Requirements", "Timeline", "Risks", "Team", "Meeting Notes"];
52const RECORD_TITLES = ["Draft launch announcement", "Schedule launch review"];
53
54type PageRow = { id: string; title: string; parentId: string | null };
55type BlockRow = { id: string; type: string; content?: { text?: string } };
56type PropRow = { id: string; name: string; type: string; isTitle?: boolean; config?: { options?: { name: string; color?: string }[] } };
57type RecordRow = { id: string; title?: string; pageId: string | null; properties: Record<string, unknown> };
58
59function unwrap<T>(body: T | { data: T }): T {
60 return Array.isArray(body) ? (body as unknown as T) : (body as { data: T }).data;
61}
62
63
64async function listPages(api: APIRequestContext, workspaceId: string, parentId?: string | null): Promise<PageRow[]> {
65 const params: Record<string, string> = {};
66 if (parentId !== undefined) params.parentId = parentId;
67 const res = await api.get(`/api/v1/workspaces/${workspaceId}/pages`, { params });
68 if (res.status() !== 200) throw new Error(`listPages ${res.status()}: ${await res.text()}`);
69 return unwrap<PageRow[]>(await res.json());
70}
71
72
73async function getPage(api: APIRequestContext, pageId: string): Promise<PageRow | null> {
74 const res = await api.get(`/api/v1/pages/${pageId}`);
75 if (res.status() !== 200) return null;
76 const body = (await res.json()) as { page?: PageRow } | PageRow;
77 return (body as { page?: PageRow }).page ?? (body as PageRow);
78}
79
80
82async function listBlocks(api: APIRequestContext, pageId: string): Promise<BlockRow[]> {
83 const res = await api.get(`/api/v1/pages/${pageId}/blocks`, { params: { depth: "all" } });
84 if (res.status() !== 200) throw new Error(`listBlocks ${res.status()}: ${await res.text()}`);
85 return unwrap<BlockRow[]>(await res.json());
86}
87
88
89async function listProperties(api: APIRequestContext, databaseId: string): Promise<PropRow[]> {
90 const res = await api.get(`/api/v1/databases/${databaseId}/properties`);
91 if (res.status() !== 200) throw new Error(`listProperties ${res.status()}: ${await res.text()}`);
92 return unwrap<PropRow[]>(await res.json());
93}
94
95
96async function listRecords(api: APIRequestContext, databaseId: string): Promise<RecordRow[]> {
97 const res = await api.get(`/api/v1/databases/${databaseId}/records`);
98 if (res.status() !== 200) throw new Error(`listRecords ${res.status()}: ${await res.text()}`);
99 const body = (await res.json()) as unknown as
100 | Array<RecordRow & { page?: { title?: string } }>
101 | { data: Array<RecordRow & { page?: { title?: string } }> };
102 return unwrap<Array<RecordRow & { page?: { title?: string } }>>(body).map((r) => ({
103 id: r.id,
104 title: r.title ?? r.page?.title,
105 pageId: r.pageId,
106 properties: r.properties
107 }));
108}
109
110test.describe("AI page generation — golden UI (TASK-0126)", () => {
111 test("Home empty-state card → Generate → page (6 headings) + Tasks database", async ({ authPage, api }) => {
112 const ws = await createWorkspace(api, `Gen ${Date.now()}`);
113
114
115
116 await authPage.goto(`/w/${ws.id}`);
117 const card = authPage.getByTestId("home-generate-card");
118 await expect(card).toBeVisible({ timeout: 30_000 });
119 const homeId = new URL(authPage.url()).pathname.split("/").pop()!;
120
121
122 await card.click();
123 const dialog = authPage.getByTestId("generate-dialog");
124 await expect(dialog).toBeVisible();
125 await authPage.getByTestId("generate-input").fill("Create a product launch plan");
126 await authPage.getByTestId("generate-submit").click();
127
128
129
130 await expect
131 .poll(
132 () => new URL(authPage.url()).pathname.split("/").pop() !== homeId,
133 { timeout: 30_000 },
134 )
135 .toBe(true);
136 const pageId = new URL(authPage.url()).pathname.split("/").pop()!;
137 await expect(dialog).toHaveCount(0);
138
139
140
141 const generated = await getPage(api, pageId);
142 expect(generated, `the generated page ${pageId} should exist`).toBeTruthy();
143 expect(generated!.title, "the generated page has the plan's title").toBe("Product Launch Plan");
144
145
146 const blocks = await listBlocks(api, pageId);
147 const headings = blocks.filter((b) => b.type === "HEADING_2").map((b) => b.content?.text);
148 for (const section of SECTIONS) {
149 expect(headings, `section heading "${section}" should be present (got ${JSON.stringify(headings)})`).toContain(section);
150 }
151 expect(headings.length, `exactly the 6 section headings (got ${headings.join(", ")})`).toBe(SECTIONS.length);
152
153
154
155 const tasksPage = (await listPages(api, ws.id, pageId)).find((p) => p.title === "Tasks");
156 expect(tasksPage, "the Tasks database page should be a child of the generated page").toBeTruthy();
157
158
159
160
161
162
163 const dbs = unwrap<{ id: string; title: string; pageId?: string }[]>(
164 await (await api.get(`/api/v1/workspaces/${ws.id}/databases`)).json(),
165 );
166 expect(dbs.length, `exactly one database in the fresh workspace (got ${dbs.map((d) => d.title).join(", ")})`).toBe(1);
167 const tasksDb = dbs[0];
168 expect(tasksDb.title, "the generated database is titled Tasks").toBe("Tasks");
169
170 const props = await listProperties(api, tasksDb!.id);
171 const byName = (n: string) => props.find((p) => p.name === n);
172 expect(byName("Title"), "a Title property (seeded)").toBeTruthy();
173 const status = byName("Status");
174 expect(status, "a Status property (seeded)").toBeTruthy();
175 expect(status!.type).toBe("SELECT");
176 expect(
177 status!.config?.options?.map((o) => o.name),
178 "the seeded Status select has To Do / In Progress / Done",
179 ).toEqual(expect.arrayContaining(["To Do", "In Progress", "Done"]));
180 const due = byName("Due");
181 expect(due, "a Due property (the plan's date column)").toBeTruthy();
182 expect(due!.type).toBe("DATE");
183
184
185
186 const records = await listRecords(api, tasksDb!.id);
187 expect(records.length, "two sample records").toBe(2);
188 expect(records.map((r) => r.title).sort()).toEqual(RECORD_TITLES.slice().sort());
189 for (const r of records) {
190 expect(r.pageId, `record "${r.title}" is a real page`).toBeTruthy();
191 const dueValue = r.properties[due!.id] as { start?: string } | undefined;
192 expect(dueValue?.start, `record "${r.title}" carries a Due date`).toMatch(/^2026-01-(15|20)$/);
193 }
194 });
195});
196