Skip to content
File

Blob: tests/e2e/specs/02-share-view-only.spec.ts

typescript139 lines
1import type { Page } from "@playwright/test";
2import { test, expect, createTestPage, createShareLink, waitForPersistedSnapshot } from "../fixtures/bland-test";
3 
4const ONE_BY_ONE_PNG = Buffer.from(
5 "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
6 "base64",
7);
8 
9async function uploadTestImage(page: Page, accessToken: string, workspaceId: string, pageId: string) {
10 const presignRes = await page.request.post(`/api/v1/workspaces/${workspaceId}/uploads/presign`, {
11 data: {
12 filename: "share-test.png",
13 content_type: "image/png",
14 size_bytes: ONE_BY_ONE_PNG.byteLength,
15 page_id: pageId,
16 },
17 headers: { Authorization: `Bearer ${accessToken}` },
18 });
19 expect(presignRes.ok()).toBeTruthy();
20 
21 const presignData = (await presignRes.json()) as {
22 upload: { upload_url: string; url: string };
23 };
24 
25 const uploadRes = await page.request.put(presignData.upload.upload_url, {
26 data: ONE_BY_ONE_PNG,
27 headers: {
28 Authorization: `Bearer ${accessToken}`,
29 "Content-Type": "image/png",
30 },
31 });
32 expect(uploadRes.ok()).toBeTruthy();
33 
34 return presignData.upload.url;
35}
36 
37test.describe("share link - view only", () => {
38 test("view-only share link shows content but prevents editing", async ({
39 authenticatedPage: { page, accessToken },
40 e2eWorkspace,
41 browser,
42 }) => {
43 // Create a page and add content as the authenticated user
44 const testPage = await createTestPage(page, accessToken, "Share View Test", e2eWorkspace);
45 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
46 
47 const editor = page.locator(".tiptap[contenteditable='true']");
48 await editor.waitFor({ timeout: 30_000 });
49 await editor.click();
50 await page.keyboard.type("Shared content visible");
51 
52 // Wait for content and sync before creating the share link
53 await expect(editor).toContainText("Shared content visible");
54 await expect(page.getByText("Connected")).toBeVisible({ timeout: 15_000 });
55 await waitForPersistedSnapshot(page, accessToken, { ...testPage, expectedText: "Shared content visible" });
56 
57 // Create a view-only share link
58 const share = await createShareLink(page, accessToken, testPage.pageId, "view");
59 
60 // Open the share link in a new unauthenticated browser context
61 const anonContext = await browser.newContext();
62 const anonPage = await anonContext.newPage();
63 await anonPage.goto(`/s/${share.token}`);
64 
65 // Wait for the editor to mount in read-only mode
66 const sharedEditor = anonPage.locator(".tiptap");
67 await sharedEditor.waitFor({ timeout: 30_000 });
68 
69 // Verify content is visible
70 await expect(sharedEditor).toContainText("Shared content visible");
71 
72 // Verify the editor is not editable
73 await expect(sharedEditor).toHaveAttribute("contenteditable", "false");
74 
75 // Verify no drag handle is present (it only renders when !readOnly)
76 await expect(anonPage.locator(".drag-handle")).toHaveCount(0);
77 
78 // Attempt to type and verify the content does not change
79 const contentBefore = await sharedEditor.textContent();
80 await sharedEditor.click();
81 await anonPage.keyboard.type("should not appear");
82 const contentAfter = await sharedEditor.textContent();
83 expect(contentAfter).toBe(contentBefore);
84 
85 await anonContext.close();
86 });
87 
88 test("shared image requests always include the share token", async ({
89 authenticatedPage: { page, accessToken },
90 e2eWorkspace,
91 browser,
92 }) => {
93 const testPage = await createTestPage(page, accessToken, "Shared Image Test", e2eWorkspace);
94 const imageUrl = await uploadTestImage(page, accessToken, testPage.workspaceId, testPage.pageId);
95 
96 await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`);
97 
98 const editor = page.locator(".tiptap[contenteditable='true']");
99 await editor.waitFor({ timeout: 30_000 });
100 await editor.evaluate((element, src) => {
101 const tiptapEditor = (
102 element as HTMLDivElement & { editor?: { commands: { setImage: (attrs: { src: string }) => boolean } } }
103 ).editor;
104 if (!tiptapEditor) {
105 throw new Error("Editor instance missing on workspace page");
106 }
107 tiptapEditor.commands.setImage({ src: src as string });
108 }, imageUrl);
109 
110 await expect(page.locator(".tiptap-image")).toHaveCount(1, { timeout: 15_000 });
111 await expect(page.getByText("Connected")).toBeVisible({ timeout: 15_000 });
112 await waitForPersistedSnapshot(page, accessToken, testPage);
113 
114 const share = await createShareLink(page, accessToken, testPage.pageId, "view");
115 
116 const anonContext = await browser.newContext();
117 const anonPage = await anonContext.newPage();
118 const uploadRequests: string[] = [];
119 
120 anonPage.on("request", (request) => {
121 const url = new URL(request.url());
122 if (url.pathname.startsWith("/uploads/")) {
123 uploadRequests.push(request.url());
124 }
125 });
126 
127 await anonPage.goto(`/s/${share.token}`);
128 await expect(anonPage.locator(".tiptap-image")).toHaveCount(1, { timeout: 30_000 });
129 await expect.poll(() => uploadRequests.length, { timeout: 10_000 }).toBeGreaterThan(0);
130 
131 const badRequests = uploadRequests.filter(
132 (requestUrl) => new URL(requestUrl).searchParams.get("share") !== share.token,
133 );
134 expect(badRequests).toEqual([]);
135 
136 await anonContext.close();
137 });
138});