Skip to content

Commit bb135d3

Browse files
bjohansebasclaude
andcommitted
test(e2e): enforce the Trusted Types CSP for real
The page is served with require-trusted-types-for 'script' and a trusted-types allowlist holding only the configured policy name — under real Chrome enforcement, which jsdom cannot do, the overlay only renders if every HTML write went through that policy (including inside the about:blank iframe, which inherits the page's CSP). The jsdom trusted-types test is removed; the helper gained a pageHeaders option. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 04968c2 commit bb135d3

3 files changed

Lines changed: 40 additions & 25 deletions

File tree

test/e2e/overlay.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,39 @@ describe("error overlay (browser)", () => {
389389
);
390390
});
391391

392+
it("renders under an enforced Trusted Types CSP with the configured policy", async () => {
393+
hotApp = await createHotApp({
394+
query: '?overlay={"trustedTypesPolicyName":"wdm-test"}',
395+
code: app("v1"),
396+
// Real enforcement, which jsdom cannot do: every HTML sink must go
397+
// through the "wdm-test" policy or Chrome throws. The about:blank
398+
// overlay iframe inherits this policy from the page.
399+
pageHeaders: {
400+
"Content-Security-Policy":
401+
"require-trusted-types-for 'script'; trusted-types wdm-test",
402+
},
403+
});
404+
({ page, browser } = await runBrowser());
405+
406+
await page.goto(hotApp.url);
407+
await page.waitForFunction(
408+
() => document.getElementById("app")?.textContent === "v1",
409+
);
410+
411+
hotApp.edit("broken by csp {{{");
412+
413+
// The overlay renders — proof that every innerHTML write went through
414+
// the configured policy (a raw write, or a policy under any other name,
415+
// would have thrown under this CSP).
416+
const frame = await waitForOverlay(page);
417+
await frame.waitForFunction(() =>
418+
document.body.textContent.includes("Module parse failed"),
419+
);
420+
expect(await frame.evaluate(() => document.body.textContent)).toContain(
421+
"broken by csp",
422+
);
423+
});
424+
392425
it("does not appear when overlay=false", async () => {
393426
hotApp = await createHotApp({ query: "?overlay=false", code: app("v1") });
394427
({ page, browser } = await runBrowser());

test/helpers/hot-app.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ function makeConfig(name, dir, appFile, query) {
6464
* (appended to the client entry) and `files` (extra fixture files by relative
6565
* name). Multi-compiler mode: pass `apps: [{ name, code }]` instead — each
6666
* app becomes a named compilation whose client connects with `?name=<name>`
67-
* and renders from `<name>.js`.
68-
* @param {{ query?: string, code?: string, files?: Record<string, string>, apps?: { name: string, code: string }[], hot?: EXPECTED_ANY }} options options
67+
* and renders from `<name>.js`. `pageHeaders` are sent with the HTML page
68+
* (e.g. a Content-Security-Policy).
69+
* @param {{ query?: string, code?: string, files?: Record<string, string>, apps?: { name: string, code: string }[], hot?: EXPECTED_ANY, pageHeaders?: Record<string, string> }} options options
6970
* @returns {Promise<EXPECTED_ANY>} handles for the running app
7071
*/
7172
async function createHotApp({
@@ -74,6 +75,7 @@ async function createHotApp({
7475
files = {},
7576
apps,
7677
hot = true,
78+
pageHeaders = {},
7779
}) {
7880
const dir = fs.mkdtempSync(
7981
path.join(fs.realpathSync.native(os.tmpdir()), "wdm-e2e-"),
@@ -136,6 +138,9 @@ async function createHotApp({
136138

137139
app.get("/", (_req, res) => {
138140
res.setHeader("Content-Type", "text/html");
141+
for (const [name, value] of Object.entries(pageHeaders)) {
142+
res.setHeader(name, value);
143+
}
139144
res.end(pageHtml(scripts));
140145
});
141146
app.use(instance);

test/overlay.test.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -324,29 +324,6 @@ describe("overlay", () => {
324324
});
325325
});
326326

327-
describe("trusted types", () => {
328-
afterEach(() => {
329-
delete globalThis.trustedTypes;
330-
});
331-
332-
it("creates a policy with the configured name and renders through it", () => {
333-
globalThis.trustedTypes = {
334-
createPolicy: jest.fn((name, rules) => ({
335-
createHTML: rules.createHTML,
336-
})),
337-
};
338-
339-
configureOverlay({ trustedTypesPolicyName: "custom#policy" });
340-
showProblems("errors", ["boom"]);
341-
342-
expect(globalThis.trustedTypes.createPolicy).toHaveBeenCalledWith(
343-
"custom#policy",
344-
expect.objectContaining({ createHTML: expect.any(Function) }),
345-
);
346-
expect(getCard().textContent).toContain("boom");
347-
});
348-
});
349-
350327
describe("configureOverlay", () => {
351328
it("returns the overlay API", () => {
352329
const api = configureOverlay({});

0 commit comments

Comments
 (0)