Skip to content
File

Blob: tests/e2e/specs/09-sidebar-move-actions.spec.ts

typescript183 lines
1import type { Locator, Page } from "@playwright/test";
2import { test, expect, createTestWorkspace } from "../fixtures/bland-test";
3 
4interface ApiPage {
5 id: string;
6 parent_id: string | null;
7 position: number;
8 title: string;
9}
10 
11function authHeaders(accessToken: string): Record<string, string> {
12 return { Authorization: `Bearer ${accessToken}` };
13}
14 
15async function createPage(
16 page: Page,
17 accessToken: string,
18 workspaceId: string,
19 title: string,
20 parentId?: string,
21): Promise<ApiPage> {
22 const data: Record<string, unknown> = { title };
23 if (parentId) data.parent_id = parentId;
24 const res = await page.request.post(`/api/v1/workspaces/${workspaceId}/pages`, {
25 data,
26 headers: authHeaders(accessToken),
27 });
28 if (!res.ok()) throw new Error(`Failed to create page "${title}": ${res.status()}`);
29 const body = (await res.json()) as { page: ApiPage };
30 return body.page;
31}
32 
33async function listPages(page: Page, accessToken: string, workspaceId: string): Promise<ApiPage[]> {
34 const res = await page.request.get(`/api/v1/workspaces/${workspaceId}/pages`, {
35 headers: authHeaders(accessToken),
36 });
37 const data = (await res.json()) as { pages: ApiPage[] };
38 return data.pages;
39}
40 
41function rowLocator(page: Page, pageId: string): Locator {
42 return page.locator(`[data-page-row][data-page-id="${pageId}"]`);
43}
44 
45async function expandRow(page: Page, pageId: string): Promise<void> {
46 const row = rowLocator(page, pageId);
47 const chevron = row.locator('button[aria-label="Expand"]');
48 if (await chevron.count()) {
49 await chevron.click().catch(() => {});
50 }
51}
52 
53async function openRowMenu(page: Page, pageId: string): Promise<void> {
54 const row = rowLocator(page, pageId);
55 await row.locator('button[aria-label="Page options"]').click();
56}
57 
58async function clickMenuItem(page: Page, label: string): Promise<void> {
59 await page.getByRole("menuitem", { name: label, exact: true }).click();
60}
61 
62test.describe("sidebar move actions", () => {
63 test("reorders root pages with Move down", async ({ authenticatedPage: { page, accessToken } }) => {
64 const workspace = await createTestWorkspace(page, accessToken, "Move Actions");
65 const a = await createPage(page, accessToken, workspace.workspaceId, "Root A");
66 const b = await createPage(page, accessToken, workspace.workspaceId, "Root B");
67 const c = await createPage(page, accessToken, workspace.workspaceId, "Root C");
68 
69 await page.goto(`/${workspace.workspaceSlug}`);
70 await rowLocator(page, a.id).waitFor({ timeout: 15_000 });
71 await rowLocator(page, c.id).waitFor({ timeout: 15_000 });
72 
73 await openRowMenu(page, b.id);
74 await clickMenuItem(page, "Move down");
75 
76 await expect
77 .poll(
78 async () => {
79 const pages = await listPages(page, accessToken, workspace.workspaceId);
80 return pages
81 .filter((candidate) => [a.id, b.id, c.id].includes(candidate.id) && candidate.parent_id === null)
82 .sort((x, y) => x.position - y.position)
83 .map((candidate) => candidate.id);
84 },
85 { timeout: 15_000 },
86 )
87 .toEqual([a.id, c.id, b.id]);
88 });
89 
90 test("indents a page into the previous sibling", async ({ authenticatedPage: { page, accessToken } }) => {
91 const workspace = await createTestWorkspace(page, accessToken, "Move Actions");
92 const parent = await createPage(page, accessToken, workspace.workspaceId, "Parent");
93 const sibling = await createPage(page, accessToken, workspace.workspaceId, "Sibling");
94 
95 await page.goto(`/${workspace.workspaceSlug}`);
96 await rowLocator(page, parent.id).waitFor({ timeout: 15_000 });
97 await rowLocator(page, sibling.id).waitFor({ timeout: 15_000 });
98 
99 await openRowMenu(page, sibling.id);
100 await clickMenuItem(page, "Indent");
101 
102 await expect
103 .poll(
104 async () => {
105 const pages = await listPages(page, accessToken, workspace.workspaceId);
106 return pages.find((candidate) => candidate.id === sibling.id)?.parent_id ?? null;
107 },
108 { timeout: 15_000 },
109 )
110 .toBe(parent.id);
111 });
112 
113 test("outdents a child to after its former parent subtree", async ({ authenticatedPage: { page, accessToken } }) => {
114 const workspace = await createTestWorkspace(page, accessToken, "Move Actions");
115 const parent = await createPage(page, accessToken, workspace.workspaceId, "Outdent Parent");
116 const childOne = await createPage(page, accessToken, workspace.workspaceId, "Child One", parent.id);
117 const childTwo = await createPage(page, accessToken, workspace.workspaceId, "Child Two", parent.id);
118 const tail = await createPage(page, accessToken, workspace.workspaceId, "Tail Root");
119 
120 await page.goto(`/${workspace.workspaceSlug}`);
121 await rowLocator(page, parent.id).waitFor({ timeout: 15_000 });
122 await expandRow(page, parent.id);
123 await rowLocator(page, childOne.id).waitFor({ timeout: 15_000 });
124 
125 await openRowMenu(page, childOne.id);
126 await clickMenuItem(page, "Outdent");
127 
128 await expect
129 .poll(
130 async () => {
131 const pages = await listPages(page, accessToken, workspace.workspaceId);
132 const rootOrder = pages
133 .filter(
134 (candidate) => [parent.id, childOne.id, tail.id].includes(candidate.id) && candidate.parent_id === null,
135 )
136 .sort((x, y) => x.position - y.position)
137 .map((candidate) => candidate.id);
138 const remainingChildren = pages
139 .filter(
140 (candidate) => [childOne.id, childTwo.id].includes(candidate.id) && candidate.parent_id === parent.id,
141 )
142 .sort((x, y) => x.position - y.position)
143 .map((candidate) => candidate.id);
144 return { rootOrder, remainingChildren };
145 },
146 { timeout: 15_000 },
147 )
148 .toEqual({
149 rootOrder: [parent.id, childOne.id, tail.id],
150 remainingChildren: [childTwo.id],
151 });
152 });
153 
154 test("moves a page into another branch with Move…", async ({ authenticatedPage: { page, accessToken } }) => {
155 const workspace = await createTestWorkspace(page, accessToken, "Move Actions");
156 const alpha = await createPage(page, accessToken, workspace.workspaceId, "Alpha");
157 const beta = await createPage(page, accessToken, workspace.workspaceId, "Beta");
158 
159 await page.goto(`/${workspace.workspaceSlug}`);
160 await rowLocator(page, alpha.id).waitFor({ timeout: 15_000 });
161 await rowLocator(page, beta.id).waitFor({ timeout: 15_000 });
162 
163 await openRowMenu(page, beta.id);
164 await clickMenuItem(page, "Move…");
165 
166 const dialog = page.getByRole("dialog");
167 await expect(dialog).toBeVisible();
168 await dialog.getByRole("button").filter({ hasText: "Alpha" }).first().click();
169 await dialog.getByRole("button", { name: "Inside", exact: true }).click();
170 await dialog.getByRole("button", { name: "Move", exact: true }).click();
171 
172 await expect
173 .poll(
174 async () => {
175 const pages = await listPages(page, accessToken, workspace.workspaceId);
176 return pages.find((candidate) => candidate.id === beta.id)?.parent_id ?? null;
177 },
178 { timeout: 15_000 },
179 )
180 .toBe(alpha.id);
181 });
182});