66 lines
3.8 KiB
TypeScript
66 lines
3.8 KiB
TypeScript
import { expect, test, type BrowserContext } from "@playwright/test";
|
|
import { readFile } from "node:fs/promises";
|
|
|
|
const apiUrl = "http://localhost:5302/api";
|
|
const networkBoundaries = new WeakMap<BrowserContext, { abortedFontStylesheets: string[]; unexpectedExternalRequests: string[] }>();
|
|
|
|
test.beforeEach(async ({ context }) => {
|
|
const boundary = { abortedFontStylesheets: [] as string[], unexpectedExternalRequests: [] as string[] };
|
|
networkBoundaries.set(context, boundary);
|
|
await context.route("**/*", route => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
if (url.protocol === "https:" && url.hostname === "fonts.googleapis.com" && url.port === "" && url.pathname === "/css2" && request.method() === "GET" && request.resourceType() === "stylesheet") {
|
|
boundary.abortedFontStylesheets.push(url.href);
|
|
return route.abort();
|
|
}
|
|
if (["http:", "https:"].includes(url.protocol) && (!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) || !["3300", "5302"].includes(url.port))) {
|
|
boundary.unexpectedExternalRequests.push(`${request.method()} ${url.origin}${url.pathname}`);
|
|
return route.abort();
|
|
}
|
|
return route.continue();
|
|
});
|
|
});
|
|
|
|
test.afterEach(async ({ context }, info) => {
|
|
const boundary = networkBoundaries.get(context);
|
|
await info.attach("csv-export-network-boundary", { body: JSON.stringify(boundary), contentType: "application/json" });
|
|
expect(boundary?.unexpectedExternalRequests).toEqual([]);
|
|
networkBoundaries.delete(context);
|
|
});
|
|
|
|
test("CSV export has safe cells and headers, and browser download preserves its UTF-8 bytes", async ({ page }) => {
|
|
await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en"));
|
|
expect((await page.request.post(`${apiUrl}/auth/login`, { data: { email: "e2e@example.test", password: "E2ePassword123!" } })).status()).toBe(200);
|
|
const me = await (await page.request.get(`${apiUrl}/auth/me`)).json();
|
|
const csrf = (await page.context().cookies()).find(cookie => cookie.name === "XSRF-TOKEN")!.value;
|
|
const headers = { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext };
|
|
const companyName = `B05 Ærlig Økonomi Ås ${Date.now()}`;
|
|
const createdCompany = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: companyName } });
|
|
expect(createdCompany.status()).toBe(201);
|
|
const company = await createdCompany.json();
|
|
const createdJob = await page.request.post(`${apiUrl}/jobapplications`, {
|
|
headers, data: { companyId: company.id, jobTitle: "=1+1", status: "Saved", notes: " \t+SUM(1,2)\r\næøå" },
|
|
});
|
|
expect(createdJob.status()).toBe(201);
|
|
const job = await createdJob.json();
|
|
|
|
const response = await page.request.get(`${apiUrl}/export/jobs?format=csv`);
|
|
expect(response.status()).toBe(200);
|
|
expect(response.headers()["content-type"]).toBe("text/csv; charset=utf-8");
|
|
expect(response.headers()["content-disposition"]).toMatch(/^attachment; filename=job-tracker-export-\d{4}-\d{2}-\d{2}\.csv;/);
|
|
const bytes = await response.body();
|
|
expect([...bytes.subarray(0, 3)]).toEqual([0xef, 0xbb, 0xbf]);
|
|
expect(bytes.toString("utf8")).toContain(`${companyName},,,'=1+1,Saved,`);
|
|
expect(bytes.toString("utf8")).toContain('"\' \t+SUM(1,2)\r\næøå"');
|
|
const raw = await (await page.request.get(`${apiUrl}/export/jobs?format=json`)).json();
|
|
expect(raw.find((item: { Id: number }) => item.Id === job.id)).toMatchObject({ JobTitle: "=1+1", Notes: " \t+SUM(1,2)\r\næøå" });
|
|
|
|
await page.goto("/settings");
|
|
const pending = page.waitForEvent("download");
|
|
await page.getByRole("button", { name: "Export CSV", exact: true }).click();
|
|
const download = await pending;
|
|
expect(download.suggestedFilename()).toMatch(/^job-tracker-export-\d{4}-\d{2}-\d{2}\.csv$/);
|
|
expect(await readFile((await download.path())!)).toEqual(bytes);
|
|
});
|