File
Blob: tests/e2e/specs/30-image-brightness.spec.ts
| 1 | import * as Y from "yjs"; |
| 2 | import { |
| 3 | test, |
| 4 | expect, |
| 5 | createTestPage, |
| 6 | createTestWorkspace, |
| 7 | createShareLink, |
| 8 | waitForDocEditorReady, |
| 9 | } from "../fixtures/bland-test"; |
| 10 | |
| 11 | test("image brightness supports keyboard input, reset, persistence, sharing, and publishing", async ({ |
| 12 | authenticatedPage: { page, accessToken }, |
| 13 | e2eContext, |
| 14 | browser, |
| 15 | }) => { |
| 16 | test.setTimeout(120_000); |
| 17 | const workspace = await createTestWorkspace(page, accessToken, "Image Brightness"); |
| 18 | const testPage = await createTestPage(page, accessToken, "Image Brightness", workspace); |
| 19 | await page.goto(`/${testPage.workspaceSlug}/${testPage.pageId}`); |
| 20 | const editor = await waitForDocEditorReady(page, { editable: true, connected: true }); |
| 21 | await editor.click(); |
| 22 | await editor.evaluate(async (element) => { |
| 23 | const canvas = document.createElement("canvas"); |
| 24 | canvas.width = 640; |
| 25 | canvas.height = 400; |
| 26 | const context = canvas.getContext("2d")!; |
| 27 | context.fillStyle = "#f4f2eb"; |
| 28 | context.fillRect(0, 0, canvas.width, canvas.height); |
| 29 | context.fillStyle = "#243129"; |
| 30 | context.font = "32px sans-serif"; |
| 31 | context.fillText("Image brightness", 40, 80); |
| 32 | const blob = await new Promise<Blob>((resolve, reject) => { |
| 33 | canvas.toBlob((result) => (result ? resolve(result) : reject(new Error("PNG fixture failed"))), "image/png"); |
| 34 | }); |
| 35 | const clipboard = new DataTransfer(); |
| 36 | clipboard.items.add(new File([blob], "first.png", { type: "image/png" })); |
| 37 | clipboard.items.add(new File([blob], "second.png", { type: "image/png" })); |
| 38 | element.dispatchEvent(new ClipboardEvent("paste", { bubbles: true, cancelable: true, clipboardData: clipboard })); |
| 39 | }); |
| 40 | |
| 41 | const images = editor.locator("img.tiptap-image"); |
| 42 | await expect(images).toHaveCount(2); |
| 43 | await expect(images.first()).toBeVisible(); |
| 44 | await expect(images.last()).toBeVisible(); |
| 45 | const originalSrc = await images.first().getAttribute("src"); |
| 46 | await images.first().click(); |
| 47 | const slider = page.getByRole("slider", { name: "Image brightness" }); |
| 48 | await expect(slider).toHaveValue("100"); |
| 49 | await slider.focus(); |
| 50 | await slider.press("ArrowLeft"); |
| 51 | await expect(slider).toHaveValue("99"); |
| 52 | await expect(slider).toBeFocused(); |
| 53 | await expect(images.first()).toHaveCSS("filter", "brightness(0.99)"); |
| 54 | |
| 55 | const reset = page.getByRole("button", { name: "Reset brightness" }); |
| 56 | await reset.focus(); |
| 57 | await reset.press("Enter"); |
| 58 | await expect(slider).toHaveValue("100"); |
| 59 | await expect(images.first()).toHaveCSS("filter", "none"); |
| 60 | |
| 61 | await page.setViewportSize({ width: 390, height: 844 }); |
| 62 | await images.first().scrollIntoViewIfNeeded(); |
| 63 | await images.first().click(); |
| 64 | await slider.focus(); |
| 65 | for (let step = 0; step < 25; step++) await slider.press("ArrowLeft"); |
| 66 | await expect(slider).toHaveValue("75"); |
| 67 | await expect(images.first()).toHaveCSS("filter", "brightness(0.75)"); |
| 68 | await expect(images.last()).toHaveCSS("filter", "none"); |
| 69 | await expect(images.first()).toHaveAttribute("src", originalSrc!); |
| 70 | const bounds = await page.locator(".tiptap-image-toolbar").boundingBox(); |
| 71 | expect(bounds).not.toBeNull(); |
| 72 | expect(bounds!.x).toBeGreaterThanOrEqual(0); |
| 73 | expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390); |
| 74 | expect(bounds!.y).toBeGreaterThanOrEqual(0); |
| 75 | expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(844); |
| 76 | await page.screenshot({ path: test.info().outputPath("image-brightness-mobile.png") }); |
| 77 | |
| 78 | const headers = { Authorization: `Bearer ${accessToken}` }; |
| 79 | await expect |
| 80 | .poll( |
| 81 | async () => { |
| 82 | const response = await page.request.get( |
| 83 | `/api/v1/workspaces/${workspace.workspaceId}/pages/${testPage.pageId}/snapshot`, |
| 84 | { headers }, |
| 85 | ); |
| 86 | if (response.status() !== 200) return []; |
| 87 | const document = new Y.Doc(); |
| 88 | try { |
| 89 | Y.applyUpdate(document, new Uint8Array(await response.body())); |
| 90 | return document |
| 91 | .getXmlFragment("document-store") |
| 92 | .toArray() |
| 93 | .filter((node): node is Y.XmlElement => node instanceof Y.XmlElement && node.nodeName === "image") |
| 94 | .map((node) => Number(node.getAttribute("brightness") ?? 100)); |
| 95 | } finally { |
| 96 | document.destroy(); |
| 97 | } |
| 98 | }, |
| 99 | { timeout: 15_000 }, |
| 100 | ) |
| 101 | .toEqual([75, 100]); |
| 102 | |
| 103 | await page.reload(); |
| 104 | await waitForDocEditorReady(page, { editable: true, connected: true }); |
| 105 | await expect(images.first()).toHaveCSS("filter", "brightness(0.75)"); |
| 106 | await expect(images.last()).toHaveCSS("filter", "none"); |
| 107 | |
| 108 | const share = await createShareLink(page, accessToken, testPage.pageId, "view"); |
| 109 | const anonymous = await browser.newContext(); |
| 110 | try { |
| 111 | const viewer = await anonymous.newPage(); |
| 112 | await viewer.goto(`${e2eContext.baseUrl}/s/${share.token}`); |
| 113 | await waitForDocEditorReady(viewer, { editable: false }); |
| 114 | await expect(viewer.locator(".tiptap img.tiptap-image").first()).toHaveCSS("filter", "brightness(0.75)"); |
| 115 | await expect(viewer.getByRole("slider", { name: "Image brightness" })).toHaveCount(0); |
| 116 | |
| 117 | const slug = `brightness-${Date.now().toString(36)}`; |
| 118 | const site = await page.request.patch(`/api/v1/workspaces/${workspace.workspaceId}/site`, { |
| 119 | headers, |
| 120 | data: { slug, published: true }, |
| 121 | }); |
| 122 | expect(site.ok()).toBe(true); |
| 123 | const publish = await page.request.post( |
| 124 | `/api/v1/workspaces/${workspace.workspaceId}/site/pages/${testPage.pageId}`, |
| 125 | { headers }, |
| 126 | ); |
| 127 | expect(publish.ok()).toBe(true); |
| 128 | // Allow the save projection to pass through the configured five-second queue batch. |
| 129 | await page.waitForTimeout(6500); |
| 130 | const { port } = new URL(e2eContext.baseUrl); |
| 131 | const publicUrl = `http://${slug}.bland.localhost${port ? `:${port}` : ""}/image-brightness-${testPage.pageId.toLowerCase()}`; |
| 132 | const response = await viewer.goto(publicUrl); |
| 133 | expect(response?.status()).toBe(200); |
| 134 | await expect(viewer.locator(".tiptap img.tiptap-image").first()).toHaveCSS("filter", "brightness(0.75)"); |
| 135 | await expect(viewer.locator(".tiptap img.tiptap-image").last()).toHaveCSS("filter", "none"); |
| 136 | await expect(viewer.getByRole("slider", { name: "Image brightness" })).toHaveCount(0); |
| 137 | } finally { |
| 138 | await anonymous.close(); |
| 139 | } |
| 140 | }); |