165 lines
5.4 KiB
TypeScript
165 lines
5.4 KiB
TypeScript
import { expect, test, type Locator, type Page } from "@playwright/test";
|
|
import {
|
|
createMalformedDocx,
|
|
createMinimalDocx,
|
|
createMinimalPptx,
|
|
createMinimalXlsx,
|
|
type OoxmlFixture,
|
|
} from "../fixtures/ooxml";
|
|
|
|
const entry = "/deep/nested/office/";
|
|
|
|
async function openFixture(page: Page, fixture: OoxmlFixture) {
|
|
await page.goto(entry);
|
|
await page
|
|
.getByTestId("office-file-input")
|
|
.setInputFiles(
|
|
fixture as unknown as Parameters<Locator["setInputFiles"]>[0],
|
|
);
|
|
}
|
|
|
|
async function expectRenderedPackage(
|
|
page: Page,
|
|
format: "docx" | "xlsx" | "pptx",
|
|
) {
|
|
await expect(page.getByTestId("opened-file")).toBeVisible();
|
|
await expect(page.getByText("Rendered locally", { exact: true })).toBeVisible(
|
|
{
|
|
timeout: 90_000,
|
|
},
|
|
);
|
|
await expect(
|
|
page.locator(`.office-render-stage--${format} canvas`).first(),
|
|
).toBeVisible();
|
|
|
|
const canvas = page.locator(`.office-render-stage--${format} canvas`).first();
|
|
const dimensions = await canvas.evaluate((element) => {
|
|
const rendered = element as HTMLCanvasElement;
|
|
const bounds = rendered.getBoundingClientRect();
|
|
return {
|
|
bitmapHeight: rendered.height,
|
|
bitmapWidth: rendered.width,
|
|
cssHeight: bounds.height,
|
|
cssWidth: bounds.width,
|
|
};
|
|
});
|
|
expect(dimensions.bitmapWidth).toBeGreaterThan(1);
|
|
expect(dimensions.bitmapHeight).toBeGreaterThan(1);
|
|
expect(dimensions.cssWidth).toBeGreaterThan(1);
|
|
expect(dimensions.cssHeight).toBeGreaterThan(1);
|
|
}
|
|
|
|
async function expectOneSearchMatch(page: Page, text: string) {
|
|
await page.getByRole("search").getByPlaceholder("Find in file").fill(text);
|
|
await page.getByRole("search").getByRole("button", { name: "Find" }).click();
|
|
await expect(page.getByText("1 match", { exact: true })).toBeVisible();
|
|
}
|
|
|
|
test("opens, renders, searches and navigates a project-authored DOCX", async ({
|
|
page,
|
|
}) => {
|
|
const externalHosts = new Set<string>();
|
|
const runtimeErrors: string[] = [];
|
|
const wasmResponses: Array<{
|
|
cacheControl: string;
|
|
contentType: string;
|
|
url: URL;
|
|
}> = [];
|
|
page.on("request", (request) => {
|
|
const url = new URL(request.url());
|
|
if (url.hostname !== "127.0.0.1") externalHosts.add(url.hostname);
|
|
});
|
|
page.on("response", (response) => {
|
|
const url = new URL(response.url());
|
|
if (url.pathname.endsWith(".wasm")) {
|
|
wasmResponses.push({
|
|
cacheControl: response.headers()["cache-control"] ?? "",
|
|
contentType: response.headers()["content-type"] ?? "",
|
|
url,
|
|
});
|
|
}
|
|
});
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") runtimeErrors.push(message.text());
|
|
});
|
|
page.on("pageerror", (error) => runtimeErrors.push(error.message));
|
|
|
|
await openFixture(page, createMinimalDocx());
|
|
await expectRenderedPackage(page, "docx");
|
|
await expect(page.getByText(/page 1 of [2-9]\d*/i)).toBeVisible();
|
|
|
|
await expectOneSearchMatch(page, "Second Page Search Marker");
|
|
await page.getByRole("button", { name: "Next page" }).click();
|
|
await expect(page.getByText(/page 2 of [2-9]\d*/i)).toBeVisible();
|
|
|
|
expect(externalHosts).toEqual(new Set());
|
|
expect(wasmResponses.length).toBeGreaterThan(0);
|
|
expect(
|
|
wasmResponses.every(
|
|
({ cacheControl, contentType, url }) =>
|
|
url.origin === "http://127.0.0.1:4173" &&
|
|
url.pathname.startsWith("/deep/nested/office/assets/") &&
|
|
contentType.startsWith("application/wasm") &&
|
|
cacheControl.includes("max-age=31536000") &&
|
|
cacheControl.includes("immutable"),
|
|
),
|
|
).toBe(true);
|
|
expect(runtimeErrors).toEqual([]);
|
|
});
|
|
|
|
test("opens, renders, searches and switches sheets in a project-authored XLSX", async ({
|
|
page,
|
|
}) => {
|
|
await openFixture(page, createMinimalXlsx());
|
|
await expectRenderedPackage(page, "xlsx");
|
|
await expect(page.getByText("sheet 1 of 2", { exact: true })).toBeVisible();
|
|
|
|
await expectOneSearchMatch(page, "Office Tools Workbook Fixture");
|
|
await page.getByRole("button", { name: "Next sheet" }).click();
|
|
await expect(page.getByText("sheet 2 of 2", { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("opens, renders, searches and navigates a project-authored PPTX", async ({
|
|
page,
|
|
}) => {
|
|
await openFixture(page, createMinimalPptx());
|
|
await expectRenderedPackage(page, "pptx");
|
|
await expect(page.getByText("slide 1 of 2", { exact: true })).toBeVisible();
|
|
|
|
await expectOneSearchMatch(page, "Second Slide Search Marker");
|
|
await page.getByRole("button", { name: "Next slide" }).click();
|
|
await expect(page.getByText("slide 2 of 2", { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("fails closed with an inline error for a malformed OOXML package", async ({
|
|
page,
|
|
}) => {
|
|
await openFixture(page, createMalformedDocx());
|
|
|
|
const alert = page.getByRole("alert");
|
|
await expect(alert).toBeVisible({ timeout: 90_000 });
|
|
await expect(alert).not.toBeEmpty();
|
|
await expect(
|
|
page.locator(".file-heading__state .state-dot--error"),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("rejects a valid package whose contents do not match its extension", async ({
|
|
page,
|
|
}) => {
|
|
const workbook = createMinimalXlsx();
|
|
await openFixture(page, {
|
|
...workbook,
|
|
mimeType:
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
name: "workbook-disguised-as-document.docx",
|
|
});
|
|
|
|
const alert = page.getByRole("alert");
|
|
await expect(alert).toBeVisible({ timeout: 90_000 });
|
|
await expect(alert).not.toBeEmpty();
|
|
await expect(
|
|
page.locator(".file-heading__state .state-dot--error"),
|
|
).toBeVisible();
|
|
});
|