Skip to content
File

Blob: tests/e2e/specs/05-toolbar-popovers.spec.ts

typescript222 lines
1import type { Locator, Page } from "@playwright/test";
2import { test, expect, createTestPage, expectNoChangeFor } from "../fixtures/bland-test";
3 
4async function expectPopoverNearTrigger(panel: Locator, trigger: Locator) {
5 await expect(panel).toBeVisible();
6 
7 const [panelBox, triggerBox] = await Promise.all([panel.boundingBox(), trigger.boundingBox()]);
8 
9 expect(panelBox).not.toBeNull();
10 expect(triggerBox).not.toBeNull();
11 
12 const resolvedPanelBox = panelBox!;
13 const resolvedTriggerBox = triggerBox!;
14 const triggerBottom = resolvedTriggerBox.y + resolvedTriggerBox.height;
15 
16 expect(resolvedPanelBox.x).toBeGreaterThan(16);
17 expect(resolvedPanelBox.y).toBeGreaterThan(16);
18 expect(Math.abs(resolvedPanelBox.x - resolvedTriggerBox.x)).toBeLessThan(96);
19 expect(Math.abs(resolvedPanelBox.y - triggerBottom)).toBeLessThan(120);
20}
21 
22async function expectColorPanelGrid(panel: Locator) {
23 const panelBox = await panel.boundingBox();
24 
25 expect(panelBox).not.toBeNull();
26 
27 const resolvedPanelBox = panelBox!;
28 
29 expect(resolvedPanelBox.width).toBeGreaterThan(120);
30 expect(resolvedPanelBox.height).toBeLessThan(120);
31}
32 
33async function openFormattingToolbar(page: Page) {
34 const editor = page.locator(".tiptap[contenteditable='true']");
35 await editor.waitFor({ timeout: 30_000 });
36 await editor.click();
37 await page.keyboard.type("Some text to select");
38 await expect(editor).toContainText("Some text to select");
39 await expect(page.getByText("Connected")).toBeVisible({ timeout: 15_000 });
40 await page.keyboard.press("ControlOrMeta+A");
41 await expect(page.getByRole("button", { name: "Text color" })).toBeVisible({ timeout: 5_000 });
42}
43 
44async function createLinkFromSelection(page: Page, href: string) {
45 const toolbar = page.locator(".tiptap-toolbar");
46 await toolbar.getByRole("button", { name: "Link" }).click();
47 await page.locator(".tiptap-link-input").fill(href);
48 await toolbar.getByRole("button", { name: "Apply" }).click();
49 await expect(page.locator(".tiptap a")).toHaveCount(1);
50}
51 
52async function collapseCursorInsideFirstLink(page: Page) {
53 const success = await page.evaluate(() => {
54 const editor = document.querySelector(".tiptap");
55 const textNode = document.querySelector(".tiptap a")?.firstChild;
56 if (!(editor instanceof HTMLElement) || !(textNode instanceof Text)) return false;
57 
58 const range = document.createRange();
59 range.setStart(textNode, Math.min(1, textNode.length));
60 range.collapse(true);
61 
62 const selection = window.getSelection();
63 selection?.removeAllRanges();
64 selection?.addRange(range);
65 editor.focus();
66 document.dispatchEvent(new Event("selectionchange"));
67 return true;
68 });
69 
70 expect(success).toBe(true);
71}
72 
73async function blurEditorFocus(page: Page) {
74 const success = await page.evaluate(() => {
75 const title = document.querySelector('textarea[placeholder="Untitled"]');
76 if (!(title instanceof HTMLTextAreaElement)) return false;
77 
78 window.getSelection()?.removeAllRanges();
79 title.focus();
80 document.dispatchEvent(new Event("selectionchange"));
81 return document.activeElement === title;
82 });
83 
84 expect(success).toBe(true);
85}
86 
87test.describe("toolbar popover anchoring", () => {
88 test("text color and highlight panels anchor to the formatting toolbar buttons", async ({
89 authenticatedPage: { page, accessToken },
90 e2eWorkspace,
91 }) => {
92 const testPage = await createTestPage(page, accessToken, "Toolbar Popover Test", e2eWorkspace);
93 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
94 
95 await openFormattingToolbar(page);
96 
97 const textColorButton = page.getByRole("button", { name: "Text color" });
98 await textColorButton.click();
99 const colorPanel = page.locator(".tiptap-color-panel");
100 await expectPopoverNearTrigger(colorPanel, textColorButton);
101 await expectColorPanelGrid(colorPanel);
102 
103 const highlightButton = page.getByRole("button", { name: "Highlight" });
104 await highlightButton.click();
105 await expectPopoverNearTrigger(colorPanel, highlightButton);
106 await expectColorPanelGrid(colorPanel);
107 });
108 
109 test("code block language menu still anchors to its button", async ({
110 authenticatedPage: { page, accessToken },
111 e2eWorkspace,
112 }) => {
113 const testPage = await createTestPage(page, accessToken, "Code Block Popover Test", e2eWorkspace);
114 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
115 
116 const editor = page.locator(".tiptap[contenteditable='true']");
117 await editor.waitFor({ timeout: 30_000 });
118 await editor.click();
119 await page.keyboard.type("```ts ");
120 
121 const languageButton = page.getByRole("button", { name: /Language:/ });
122 await expect(languageButton).toBeVisible({ timeout: 5_000 });
123 await languageButton.click();
124 
125 const languageMenu = page.getByRole("menu", { name: "Code block language" });
126 await expectPopoverNearTrigger(languageMenu, languageButton);
127 
128 await expect
129 .poll(async () => {
130 return await page.evaluate(() => {
131 const dropdown = document.querySelector(".tiptap-code-block-lang-dropdown");
132 const active = document.querySelector(".tiptap-code-block-lang-item.is-active");
133 if (!(dropdown instanceof HTMLDivElement) || !(active instanceof HTMLButtonElement)) return false;
134 
135 const visibleTop = dropdown.scrollTop;
136 const visibleBottom = dropdown.scrollTop + dropdown.clientHeight;
137 const activeTop = active.offsetTop;
138 const activeBottom = active.offsetTop + active.offsetHeight;
139 
140 return dropdown.scrollTop > 0 && activeTop >= visibleTop && activeBottom <= visibleBottom;
141 });
142 })
143 .toBe(true);
144 
145 const selectedLanguageState = await page.evaluate(() => {
146 const dropdown = document.querySelector(".tiptap-code-block-lang-dropdown");
147 const active = document.querySelector(".tiptap-code-block-lang-item.is-active");
148 if (!(dropdown instanceof HTMLDivElement) || !(active instanceof HTMLButtonElement)) return null;
149 
150 const visibleTop = dropdown.scrollTop;
151 const visibleBottom = dropdown.scrollTop + dropdown.clientHeight;
152 const activeTop = active.offsetTop;
153 const activeBottom = active.offsetTop + active.offsetHeight;
154 
155 return {
156 scrollTop: dropdown.scrollTop,
157 paddingTop: getComputedStyle(dropdown).paddingTop,
158 paddingBottom: getComputedStyle(dropdown).paddingBottom,
159 fullyVisible: activeTop >= visibleTop && activeBottom <= visibleBottom,
160 };
161 });
162 
163 if (!selectedLanguageState) {
164 throw new Error("Expected selected language state after opening the code block language menu.");
165 }
166 
167 expect(selectedLanguageState.scrollTop).toBeGreaterThan(0);
168 expect(selectedLanguageState.fullyVisible).toBe(true);
169 expect(selectedLanguageState.paddingTop).toBe("4px");
170 expect(selectedLanguageState.paddingBottom).toBe("4px");
171 });
172 
173 test("link toolbar anchors to the linked text instead of the viewport origin", async ({
174 authenticatedPage: { page, accessToken },
175 e2eWorkspace,
176 }) => {
177 const testPage = await createTestPage(page, accessToken, "Link Toolbar Popover Test", e2eWorkspace);
178 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
179 
180 await openFormattingToolbar(page);
181 await createLinkFromSelection(page, "https://example.com");
182 await collapseCursorInsideFirstLink(page);
183 
184 const linkedText = page.locator(".tiptap a").first();
185 await expectPopoverNearTrigger(page.locator(".tiptap-link-toolbar"), linkedText);
186 await expect(page.getByRole("button", { name: "Edit link" })).toBeVisible({ timeout: 5_000 });
187 });
188 
189 test("link hover popup stays open when moving from the link into the toolbar", async ({
190 authenticatedPage: { page, accessToken },
191 e2eWorkspace,
192 }) => {
193 const testPage = await createTestPage(page, accessToken, "Link Hover Toolbar Test", e2eWorkspace);
194 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
195 
196 await openFormattingToolbar(page);
197 await createLinkFromSelection(page, "https://example.com");
198 await blurEditorFocus(page);
199 
200 const linkedText = page.locator(".tiptap a").first();
201 const linkToolbar = page.locator(".tiptap-link-toolbar");
202 
203 await linkedText.hover();
204 await expect(linkToolbar).toBeVisible({ timeout: 5_000 });
205 
206 const [linkBox, toolbarBox] = await Promise.all([linkedText.boundingBox(), linkToolbar.boundingBox()]);
207 
208 expect(linkBox).not.toBeNull();
209 expect(toolbarBox).not.toBeNull();
210 
211 const fromX = linkBox!.x + linkBox!.width / 2;
212 const fromY = linkBox!.y + linkBox!.height / 2;
213 const toX = toolbarBox!.x + toolbarBox!.width / 2;
214 const toY = toolbarBox!.y + toolbarBox!.height / 2;
215 
216 await page.mouse.move(fromX, fromY);
217 await page.mouse.move(toX, toY, { steps: 12 });
218 await expectNoChangeFor(() => linkToolbar.isVisible(), 300);
219 await expect(linkToolbar).toBeVisible();
220 });
221});