149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
test("loads from a deep path and keeps the last valid projection while source is invalid", async ({
|
|
page,
|
|
}) => {
|
|
const pageErrors: string[] = [];
|
|
page.on("pageerror", (error) => pageErrors.push(error.message));
|
|
|
|
await page.goto("/deep/nested/svg/");
|
|
await expect(page).toHaveTitle("SVG Tools");
|
|
await expect(
|
|
page.getByText("Source synchronized", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("tree", { name: "SVG element structure" }),
|
|
).toBeVisible();
|
|
await expect(page.getByTitle("Sanitized SVG preview")).toHaveAttribute(
|
|
"sandbox",
|
|
"allow-scripts",
|
|
);
|
|
|
|
const treeItems = page.getByRole("treeitem");
|
|
const initialTreeCount = await treeItems.count();
|
|
expect(initialTreeCount).toBeGreaterThan(1);
|
|
await page
|
|
.frameLocator('iframe[title="Sanitized SVG preview"]')
|
|
.locator("#sun")
|
|
.click();
|
|
await expect(page.locator('[data-node-key="id:sun"]')).toHaveAttribute(
|
|
"aria-selected",
|
|
"true",
|
|
);
|
|
|
|
const source = page.locator(".cm-content");
|
|
await source.click();
|
|
await page.keyboard.press("Control+A");
|
|
// insertText bypasses CodeMirror's helpful XML close-tag completion so this
|
|
// exercise really leaves the canonical source in an incomplete state.
|
|
await page.keyboard.insertText('<svg xmlns="http://www.w3.org/2000/svg"><g>');
|
|
await expect(page.getByText("Source invalid", { exact: true })).toBeVisible();
|
|
await expect(
|
|
page.getByText("Showing the last valid canvas revision."),
|
|
).toBeVisible();
|
|
expect(await treeItems.count()).toBe(initialTreeCount);
|
|
await expect(page.getByRole("treeitem").first()).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Download SVG" }).last(),
|
|
).toBeDisabled();
|
|
|
|
await source.click();
|
|
await page.keyboard.press("Control+z");
|
|
await expect(
|
|
page.getByText("Source synchronized", { exact: true }),
|
|
).toBeVisible();
|
|
expect(pageErrors).toEqual([]);
|
|
});
|
|
|
|
test("synchronizes tree selection, source patches, undo and redo", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/deep/nested/svg/");
|
|
await expect(
|
|
page.getByText("Source synchronized", { exact: true }),
|
|
).toBeVisible();
|
|
|
|
await page.getByPlaceholder("Filter elements…").fill("path");
|
|
const pathRow = page.getByRole("treeitem").filter({ hasText: "path" }).last();
|
|
await pathRow.click();
|
|
await expect(pathRow).toHaveAttribute("aria-selected", "true");
|
|
await page.getByRole("tab", { name: "Element" }).click();
|
|
|
|
const fill = page.getByLabel("Fill", { exact: true });
|
|
await fill.fill("#123456");
|
|
await fill.press("Enter");
|
|
await expect(page.locator(".cm-content")).toContainText('fill="#123456"');
|
|
|
|
await page.getByRole("button", { name: "Undo" }).click();
|
|
await expect(page.locator(".cm-content")).not.toContainText('fill="#123456"');
|
|
await page.getByRole("button", { name: "Redo" }).click();
|
|
await expect(page.locator(".cm-content")).toContainText('fill="#123456"');
|
|
});
|
|
|
|
test("connects to a valid same-origin Toolbox catalogue from the nested build", async ({
|
|
page,
|
|
}) => {
|
|
await page.goto("/deep/nested/svg/?toolbox=%2Ftoolbox.catalog.json");
|
|
await expect(
|
|
page.getByText("Source synchronized", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.locator(".toolbox-shell")).toHaveAttribute(
|
|
"data-toolbox-context",
|
|
"connected",
|
|
);
|
|
await page.getByRole("button", { name: "Apps" }).click();
|
|
const switcher = page.getByRole("navigation", {
|
|
name: "Toolbox applications",
|
|
});
|
|
await expect(switcher).toBeVisible();
|
|
await expect(
|
|
switcher.getByRole("link", { name: "SVG Tools" }),
|
|
).toHaveAttribute("aria-current", "page");
|
|
});
|
|
|
|
test("opens hostile SVG locally without executing scripts or fetching external URLs", async ({
|
|
page,
|
|
}) => {
|
|
const forbiddenRequests: string[] = [];
|
|
let executed = false;
|
|
page.on("request", (request) => {
|
|
if (request.url().includes("invalid.example"))
|
|
forbiddenRequests.push(request.url());
|
|
});
|
|
await page.exposeFunction("svgToolsExecuted", () => {
|
|
executed = true;
|
|
});
|
|
await page.goto("/deep/nested/svg/");
|
|
|
|
const hostile = `
|
|
<!DOCTYPE svg SYSTEM "https://invalid.example/tracker.dtd">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
<script>parent.svgToolsExecuted()</script>
|
|
<image href="https://invalid.example/tracker.png" width="20" height="20"/>
|
|
<rect width="20" height="20" onclick="parent.svgToolsExecuted()"/>
|
|
</svg>`;
|
|
await page.locator('input[type="file"]').evaluate((element, contents) => {
|
|
const transfer = new DataTransfer();
|
|
transfer.items.add(
|
|
new File([contents], "hostile.svg", { type: "image/svg+xml" }),
|
|
);
|
|
(element as HTMLInputElement).files = transfer.files;
|
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}, hostile);
|
|
await expect(
|
|
page.getByText("Source synchronized", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText(/blocked-script|event-handler|unsafe-url/).first(),
|
|
).toBeVisible();
|
|
|
|
const frame = page.frameLocator('iframe[title="Sanitized SVG preview"]');
|
|
await expect(frame.locator("script")).toHaveCount(0);
|
|
await expect(frame.locator("image")).not.toHaveAttribute(
|
|
"href",
|
|
/invalid\.example/u,
|
|
);
|
|
expect(executed).toBe(false);
|
|
expect(forbiddenRequests).toEqual([]);
|
|
});
|