diff --git a/apps/web/e2e/chart-links.spec.ts b/apps/web/e2e/chart-links.spec.ts index 2aa9813..d67b26d 100644 --- a/apps/web/e2e/chart-links.spec.ts +++ b/apps/web/e2e/chart-links.spec.ts @@ -3,90 +3,91 @@ import { captureScreenshot } from "./helpers"; const fixture = "/e2e/fixtures/chart-links.html"; -test.describe.serial("chart links", () => { - test("cannot execute script when clicked or expanded", async ({ page }, testInfo) => { - test.setTimeout(120_000); - // ChartCanvas lazy-loads Observable Plot; pay the import cost once before the matrix below. - await page.goto(`${fixture}?${new URLSearchParams({ href: "#details", inspect: "0" })}`); - await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 90_000 }); - for (const inspect of ["0", "1"]) { - for (const href of [ - 'javascript:void(document.body.dataset.plotExecuted="yes")', - ' \u0000\u001fJaVa\tScRi\nPt:void(document.body.dataset.plotExecuted="yes")', - 'javascript:%76oid(document.body.dataset.plotExecuted="yes")', - "data:text/html,", - "file:///example.txt", - ]) { - await page.goto(`${fixture}?${new URLSearchParams({ href, inspect })}`); - const circle = page.locator("svg circle").first(); - // Every navigation renders Plot again, which takes seconds on a loaded CI runner. - await expect(circle).toBeVisible({ timeout: 30_000 }); - const assertInert = async () => { - const links = page.locator("svg a"); - expect(await links.count()).toBeGreaterThan(0); - expect( - await links.evaluateAll((anchors) => - anchors.every( - (anchor) => - !anchor.hasAttribute("href") && - !anchor.hasAttributeNS("http://www.w3.org/1999/xlink", "href"), +test.describe + .serial("chart links", () => { + test("cannot execute script when clicked or expanded", async ({ page }, testInfo) => { + test.setTimeout(120_000); + // ChartCanvas lazy-loads Observable Plot; pay the import cost once before the matrix below. + await page.goto(`${fixture}?${new URLSearchParams({ href: "#details", inspect: "0" })}`); + await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 90_000 }); + for (const inspect of ["0", "1"]) { + for (const href of [ + 'javascript:void(document.body.dataset.plotExecuted="yes")', + ' \u0000\u001fJaVa\tScRi\nPt:void(document.body.dataset.plotExecuted="yes")', + 'javascript:%76oid(document.body.dataset.plotExecuted="yes")', + "data:text/html,", + "file:///example.txt", + ]) { + await page.goto(`${fixture}?${new URLSearchParams({ href, inspect })}`); + const circle = page.locator("svg circle").first(); + // Every navigation renders Plot again, which takes seconds on a loaded CI runner. + await expect(circle).toBeVisible({ timeout: 30_000 }); + const assertInert = async () => { + const links = page.locator("svg a"); + expect(await links.count()).toBeGreaterThan(0); + expect( + await links.evaluateAll((anchors) => + anchors.every( + (anchor) => + !anchor.hasAttribute("href") && + !anchor.hasAttributeNS("http://www.w3.org/1999/xlink", "href"), + ), ), - ), - ).toBe(true); - await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed"); - }; - await assertInert(); - await circle.click(); - // Hover inspection still works, without reintroducing unsafe links. - if (inspect === "1") await expect(page.locator('[aria-label="tip"]')).toBeVisible(); - await assertInert(); - await page.getByRole("button", { name: "Expand", exact: true }).click(); - const dialog = page.getByRole("dialog"); - await expect(dialog.locator("svg circle")).toBeVisible({ timeout: 30_000 }); - await dialog.locator("svg circle").click(); - await assertInert(); - await expect(page).toHaveURL(`${fixture}?${new URLSearchParams({ href, inspect })}`); - await page.getByRole("button", { name: "Close chart" }).click(); + ).toBe(true); + await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed"); + }; + await assertInert(); + await circle.click(); + // Hover inspection still works, without reintroducing unsafe links. + if (inspect === "1") await expect(page.locator('[aria-label="tip"]')).toBeVisible(); + await assertInert(); + await page.getByRole("button", { name: "Expand", exact: true }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog.locator("svg circle")).toBeVisible({ timeout: 30_000 }); + await dialog.locator("svg circle").click(); + await assertInert(); + await expect(page).toHaveURL(`${fixture}?${new URLSearchParams({ href, inspect })}`); + await page.getByRole("button", { name: "Close chart" }).click(); + } } - } - await captureScreenshot(page, testInfo, "chart-with-inert-link"); - }); + await captureScreenshot(page, testInfo, "chart-with-inert-link"); + }); - test("retain supported URLs and browser encoding semantics", async ({ page }) => { - test.setTimeout(120_000); - await page.route("https://example.test/**", (route) => - route.fulfill({ body: "Chart destination" }), - ); - await page.route("**/e2e/fixtures/java*", (route) => - route.fulfill({ body: "Chart destination" }), - ); - await page.goto(`${fixture}?${new URLSearchParams({ href: "#details" })}`); - await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 30_000 }); - for (const href of [ - "https://example.test/chart?a=1&b=2", - "mailto:chart@example.test", - "tel:+15555550123", - "#details", - "javascript:void(0)", - "javascript%3Avoid(0)", - "java%73cript:void(0)", - ]) { - await page.goto(`${fixture}?${new URLSearchParams({ href })}`); + test("retain supported URLs and browser encoding semantics", async ({ page }) => { + test.setTimeout(120_000); + await page.route("https://example.test/**", (route) => + route.fulfill({ body: "Chart destination" }), + ); + await page.route("**/e2e/fixtures/java*", (route) => + route.fulfill({ body: "Chart destination" }), + ); + await page.goto(`${fixture}?${new URLSearchParams({ href: "#details" })}`); await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 30_000 }); - const link = page.locator("svg a").first(); - await expect(link).toBeAttached(); - expect( - await link.evaluate((anchor) => - anchor.getAttributeNS("http://www.w3.org/1999/xlink", "href"), - ), - ).toBe(href); - const destination = new URL(href, page.url()); - expect(destination.protocol).not.toBe("javascript:"); - if (destination.protocol === "http:" || destination.protocol === "https:") { - await link.locator("circle").click(); - await expect(page).toHaveURL(destination.href); - await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed"); + for (const href of [ + "https://example.test/chart?a=1&b=2", + "mailto:chart@example.test", + "tel:+15555550123", + "#details", + "javascript:void(0)", + "javascript%3Avoid(0)", + "java%73cript:void(0)", + ]) { + await page.goto(`${fixture}?${new URLSearchParams({ href })}`); + await expect(page.locator("svg circle").first()).toBeVisible({ timeout: 30_000 }); + const link = page.locator("svg a").first(); + await expect(link).toBeAttached(); + expect( + await link.evaluate((anchor) => + anchor.getAttributeNS("http://www.w3.org/1999/xlink", "href"), + ), + ).toBe(href); + const destination = new URL(href, page.url()); + expect(destination.protocol).not.toBe("javascript:"); + if (destination.protocol === "http:" || destination.protocol === "https:") { + await link.locator("circle").click(); + await expect(page).toHaveURL(destination.href); + await expect(page.locator("body")).not.toHaveAttribute("data-plot-executed"); + } } - } + }); }); -});