1
25import { test, expect } from "../fixtures";
26import { createPage } from "../utils/api";
27
28test.describe("Pages — Phase 3", () => {
29 test("create page from sidebar", async ({ authPage, freshWorkspace }) => {
30 const ws = await freshWorkspace("Pages Space");
31 await authPage.goto(`/w/${ws.id}`);
32 await expect(authPage.getByTestId("sidebar")).toBeVisible();
33
34 await authPage.getByTestId("sidebar-new-page").click();
35
36
37 const title = authPage.getByTestId("page-title");
38 await expect(title).toBeVisible({ timeout: 10_000 });
39 await title.click();
40 await title.press("ControlOrMeta+a").catch(() => {});
41 await title.type("First Real Page");
42
43 await authPage.keyboard.press("Escape");
44
45
46 await expect(authPage.getByTestId("sidebar").getByText("First Real Page")).toBeVisible({
47 timeout: 10_000,
48 });
49 });
50
51 test("nested page: create a page inside a page", async ({
52 authPage,
53 api,
54 freshWorkspace,
55 }) => {
56 const ws = await freshWorkspace("Nesting Space");
57 const parent = await createPage(api, ws.id, { title: "Parent Page" });
58
59 await authPage.goto(`/w/${ws.id}/page/${parent.id}`);
60 await expect(authPage.getByTestId("page-title")).toContainText("Parent Page");
61
62
63
64
65
66
67
68
69
70
71
72 await authPage.getByTestId("subpage-create").click();
73
74
75 let childId: string | null = null;
76 await expect
77 .poll(
78 async () => {
79 const body = await (
80 await api.get(
81 `/api/v1/workspaces/${ws.id}/pages?parentId=${parent.id}`,
82 )
83 ).json();
84 const kids = (body.data ?? body) as { id: string }[];
85 childId = kids.length > 0 ? kids[0].id : null;
86 return childId !== null;
87 },
88 { timeout: 10_000 },
89 )
90 .toBeTruthy();
91
92
93
94 await authPage.goto(`/w/${ws.id}/page/${childId}`);
95
96
97
98
99
100 const childTitle = authPage.getByTestId("page-title");
101 await expect(childTitle).toHaveAttribute("data-page-id", childId, {
102 timeout: 10_000,
103 });
104 await childTitle.click();
105 await childTitle.type("Child Page");
106 await authPage.keyboard.press("Escape");
107
108
109
110
111
112
113 await expect(
114 authPage.getByTestId(`sidebar-page-${childId}`),
115 ).toBeVisible({ timeout: 10_000 });
116
117
118
119
120
121
122
123
124 let childRow: { id: string; title: string; parentId: string } | null = null;
125 await expect
126 .poll(
127 async () => {
128 const body = await (
129 await api.get(
130 `/api/v1/workspaces/${ws.id}/pages?parentId=${parent.id}`,
131 )
132 ).json();
133 const kids = (body.data ?? body) as {
134 id: string;
135 title: string;
136 parentId: string;
137 }[];
138 childRow = kids.find((c) => c.id === childId) ?? null;
139 return childRow?.title === "Child Page";
140 },
141 { timeout: 10_000 },
142 )
143 .toBeTruthy();
144 expect(childRow, "child page exists under the parent").toBeTruthy();
145 expect(childRow?.parentId).toBe(parent.id);
146 });
147
148 test("rename page", async ({ authPage, api, freshWorkspace }) => {
149 const ws = await freshWorkspace("Rename Page Space");
150 const page = await createPage(api, ws.id, { title: "Before Rename" });
151
152 await authPage.goto(`/w/${ws.id}/page/${page.id}`);
153 const title = authPage.getByTestId("page-title");
154 await title.click();
155 await title.press("ControlOrMeta+a").catch(() => {});
156 await title.type("After Rename");
157 await authPage.keyboard.press("Escape");
158
159 await expect(title).toContainText("After Rename", { timeout: 10_000 });
160 });
161
162 test("icon + cover", async ({ authPage, api, freshWorkspace }) => {
163 const ws = await freshWorkspace("Style Space");
164 const page = await createPage(api, ws.id, { title: "Styled Page" });
165 await authPage.goto(`/w/${ws.id}/page/${page.id}`);
166
167
168 await authPage.getByTestId("page-icon-button").click();
169 const iconPicker = authPage.getByTestId("icon-picker");
170 await expect(iconPicker).toBeVisible();
171 await iconPicker.getByText("🚀").click();
172
173 await expect(authPage.getByTestId("page-icon")).toContainText("🚀", {
174 timeout: 10_000,
175 });
176
177
178 await authPage.getByTestId("page-cover-picker").click();
179 const coverPicker = authPage.getByTestId("cover-picker");
180 await expect(coverPicker).toBeVisible();
181 await coverPicker.getByTestId("cover-color-0").click();
182 await expect(authPage.getByTestId("page-cover")).toBeVisible();
183 });
184
185 test("move page via context menu", async ({ authPage, api, freshWorkspace }) => {
186 const ws = await freshWorkspace("Move Space");
187 const page = await createPage(api, ws.id, { title: "Mover" });
188 const destination = await createPage(api, ws.id, { title: "Destination" });
189
190 await authPage.goto(`/w/${ws.id}`);
191 await authPage.getByTestId("sidebar").getByText("Mover").click();
192
193
194 await authPage
195 .getByTestId("sidebar-page-" + page.id)
196 .click({ button: "right" });
197 await authPage.getByTestId("page-context-menu").getByTestId("context-menu-move").click();
198 const moveDialog = authPage.getByTestId("move-dialog");
199 await expect(moveDialog).toBeVisible();
200 await moveDialog.getByText("Destination").click();
201 await moveDialog.getByTestId("move-dialog-confirm").click();
202
203
204
205
206
207 await expect(moveDialog).not.toBeVisible({ timeout: 10_000 });
208
209
210 const moved = await api.get(`/api/v1/pages/${page.id}`);
211 const body = await moved.json();
212 const mover = body.page ?? body;
213 expect(mover.parentId).toBe(destination.id);
214 });
215
216 test("trash → restore → purge round-trip", async ({
217 authPage,
218 api,
219 freshWorkspace,
220 }) => {
221 const ws = await freshWorkspace("Trash Space");
222 const page = await createPage(api, ws.id, { title: "Trash Candidate" });
223
224 await api.post(`/api/v1/pages/${page.id}/blocks`, {
225 data: { type: "PARAGRAPH", content: { text: "precious content" } },
226 });
227
228
229 await authPage.goto(`/w/${ws.id}`);
230 await authPage
231 .getByTestId("sidebar-page-" + page.id)
232 .click({ button: "right" });
233 await authPage.getByTestId("page-context-menu").getByTestId("context-menu-trash").click();
234 await expect(
235 authPage.getByTestId("sidebar").getByText("Trash Candidate"),
236 ).toHaveCount(0);
237
238
239 let body = await (await api.get(`/api/v1/pages/${page.id}`)).json();
240 let p = body.page ?? body;
241 expect(p.trashedAt).toBeTruthy();
242 expect(p.deletedAt).toBeFalsy();
243
244
245 await authPage.getByTestId("trash-section").click();
246 const item = authPage.getByTestId(`trash-item-${page.id}`);
247 await expect(item).toBeVisible({ timeout: 10_000 });
248 await authPage.getByTestId(`trash-restore-${page.id}`).click();
249
250
251
252
253 await expect(item).toHaveCount(0, { timeout: 10_000 });
254
255 body = await (await api.get(`/api/v1/pages/${page.id}`)).json();
256 p = body.page ?? body;
257 expect(p.trashedAt).toBeFalsy();
258 expect(p.restoredAt).toBeTruthy();
259
260
261
262
263
264
265
266 const blocks = await (
267 await api.get(`/api/v1/pages/${page.id}/blocks`)
268 ).json();
269 const blockRows = (blocks.data ?? blocks.blocks ?? blocks) as Array<{
270 content?: { text?: string };
271 }>;
272 expect(blockRows.some((b) => b.content?.text === "precious content")).toBe(true);
273
274
275
276
277
278 await authPage
279 .getByTestId("sidebar-page-" + page.id)
280 .click({ button: "right" });
281 await authPage.getByTestId("page-context-menu").getByTestId("context-menu-trash").click();
282 await authPage.getByTestId("trash-section").click();
283 const itemAgain = authPage.getByTestId(`trash-item-${page.id}`);
284 await expect(itemAgain).toBeVisible({ timeout: 10_000 });
285 await authPage.getByTestId(`trash-purge-${page.id}`).click();
286
287 await expect(itemAgain).toHaveCount(0, { timeout: 10_000 });
288
289 const gone = await api.get(`/api/v1/pages/${page.id}`);
290 expect(gone.status()).toBe(404);
291 });
292
293 test("favorite page appears in favorites section", async ({
294 authPage,
295 api,
296 freshWorkspace,
297 }) => {
298
299
300
301
302 const ws = await freshWorkspace("Star Space");
303 const page = await createPage(api, ws.id, { title: "Starred Page" });
304
305 await authPage.goto(`/w/${ws.id}`);
306 await authPage
307 .getByTestId("sidebar-page-" + page.id)
308 .click({ button: "right" });
309 await authPage
310 .getByTestId("page-context-menu")
311 .getByTestId("context-menu-favorite")
312 .click();
313
314 await authPage.getByTestId("sidebar").getByText("Favorites").click();
315 await expect(
316 authPage.getByTestId(`favorite-item-${page.id}`),
317 ).toBeVisible({ timeout: 10_000 });
318 });
319
320 test("version snapshot visible after edit", async ({
321 authPage,
322 api,
323 freshWorkspace,
324 }) => {
325 const ws = await freshWorkspace("Versions Space");
326 const page = await createPage(api, ws.id, { title: "Versioned Page" });
327
328
329 await api.post(`/api/v1/pages/${page.id}/blocks`, {
330 data: { type: "PARAGRAPH", content: { text: "v1 content" } },
331 });
332
333 await authPage.goto(`/w/${ws.id}/page/${page.id}`);
334
335 await authPage.getByTestId("page-more-menu").click();
336 await authPage.getByTestId("page-more-history").click();
337 const versions = authPage.getByTestId("page-versions");
338 await expect(versions).toBeVisible({ timeout: 10_000 });
339
340
341
342
343
344
345 const entries = versions.getByTestId("page-version-entry");
346 await versions.getByTestId("version-snapshot-button").click();
347 await expect(entries).toHaveCount(1, { timeout: 10_000 });
348 await versions.getByTestId("version-snapshot-button").click();
349 await expect(entries).toHaveCount(2, { timeout: 10_000 });
350 });
351});
352