// HTML tests are tests relating to HTML files themselves. import { devTest, emptyHtmlFile } from "../bake-harness"; devTest("html file is watched", { files: { "index.html": emptyHtmlFile({ scripts: ["/script.ts"], body: "

Hello

", }), "script.ts": ` console.log("hello"); `, }, async test(dev) { await dev.fetch("/").expect.toInclude("

Hello

"); await dev.fetch("/").expect.toInclude("

Hello

"); await dev.patch("index.html", { find: "Hello", replace: "World", }); await dev.fetch("/").expect.toInclude("

World

"); // Works await using c = await dev.client("/"); await c.expectMessage("hello"); // Editing HTML reloads await c.expectReload(async () => { await dev.patch("index.html", { find: "World", replace: "Hello", }); await dev.fetch("/").expect.toInclude("

Hello

"); }); await c.expectMessage("hello"); await c.expectReload(async () => { await dev.patch("index.html", { find: "Hello", replace: "Bar", }); await dev.fetch("/").expect.toInclude("

Bar

"); }); await c.expectMessage("hello"); await c.expectReload(async () => { await dev.patch("script.ts", { find: "hello", replace: "world", }); }); await c.expectMessage("world"); }, }); devTest("image tag", { files: { "index.html": ` test image `, "image.png": "FIRST", }, async test(dev) { await using c = await dev.client("/"); const url: string = await c.js`document.querySelector("img").src`; expect(url).toBeString(); // image tag exists await dev.fetch(url).expect.toBe("FIRST"); // Editing HTML causes reload but image still works await c.expectReload(async () => { await dev.patch("index.html", { find: 'alt="test image"', replace: 'alt="modified image"', }); await dev.fetch("/").expect.toInclude('alt="modified image"'); }); // Editing image content causes a hard reload because the html must reflect the new image content await c.expectReload(async () => { await dev.patch("image.png", { find: "FIRST", replace: "SECOND", }); }); const url2 = await c.js`document.querySelector("img").src`; expect(url).not.toBe(url2); await dev.fetch(url2).expect.toBe("SECOND"); await dev.fetch(url).expect404(); // TODO }, }); devTest("image import in JS", { files: { "index.html": ` `, "script.ts": ` import img from "./image.png"; console.log(img); `, "image.png": "FIRST", }, async test(dev) { await using c = await dev.client("/"); const img1 = await c.getStringMessage(); await dev.fetch(img1).expect.toBe("FIRST"); // Editing image content updates the image URL await c.expectReload(async () => { await dev.patch("image.png", { find: "FIRST", replace: "SECOND", }); }); const img2 = await c.getStringMessage(); await dev.fetch(img2).expect.toBe("SECOND"); // await dev.fetch(img1).expect404(); }, }); devTest("import then create", { files: { "index.html": ` `, "script.ts": ` import data from "./data"; console.log(data); `, }, async test(dev) { const c = await dev.client("/", { errors: ['script.ts:1:18: error: Could not resolve: "./data"'], }); await c.expectReload(async () => { await dev.write("data.ts", "export default 'data';"); }); await c.expectMessage("data"); }, }); devTest("external links", { files: { "index.html": ` index | Powered by Bun
`, "index.css": ` body { background-color: red; } `, "index.client.tsx": ` console.log("hello"); `, }, async test(dev) { await using c = await dev.client("/"); await c.expectMessage("hello"); const ico: string = await c.js`document.querySelector("link[rel='icon']").href`; expect(ico).toBe("https://bun.sh/favicon.ico"); }, }); devTest("memory leak case 1", { files: { "index.html": ` `, "script.ts": ` import data from "./data"; `, }, async test(dev) { await dev.fetch("/"); // previously leaked source map }, }); devTest("chrome devtools automatic workspace folders", { files: { "index.html": ` `, "script.ts": ` console.log("hello"); `, }, async test(dev) { const response = await dev.fetch("/.well-known/appspecific/com.chrome.devtools.json"); expect(response.status).toBe(200); const json = await response.json(); const root = dev.join("."); expect(json).toMatchObject({ workspace: { root, uuid: expect.any(String), }, }); }, }); devTest("error report endpoint handles stack frames with very long absolute paths", { files: { "index.html": emptyHtmlFile({ scripts: ["/script.ts"], body: "

Error Report

", }), "script.ts": ` console.log("hello"); `, }, async test(dev) { // Wire format of POST /_bun/report_error (length-prefixed binary): // string32 error name, string32 message, string32 browser url, // u32 frame count, then per frame: i32 line, i32 column, // string32 function name, string32 file name. function u32(n: number) { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b; } function i32(n: number) { const b = Buffer.alloc(4); b.writeInt32LE(n, 0); return b; } function str32(s: string) { const bytes = Buffer.from(s, "utf8"); return Buffer.concat([u32(bytes.length), bytes]); } function frame(line: number, column: number, functionName: string, fileName: string) { return Buffer.concat([i32(line), i32(column), str32(functionName), str32(fileName)]); } // One ordinary frame pointing at a real project file, plus one frame whose // absolute path is far larger than any platform path buffer (16 KiB). const normalPath = dev.join("script.ts"); const oversizedPath = "/" + "A/".repeat(8192); const body = Buffer.concat([ str32("Error"), // error name str32("test message"), // error message str32(dev.baseUrl + "/"), // browser url u32(2), // stack frame count frame(1, 1, "first", normalPath), frame(1, 1, "second", oversizedPath), ]); const res = await dev.fetch("/_bun/report_error", { method: "POST", body }); expect(res.status).toBe(200); // The reply still references the legitimate frame's file. const text = await res.text(); expect(text).toContain("script.ts"); // The dev server must still be serving requests afterwards. await dev.fetch("/").expect.toInclude("

Error Report

"); }, }); devTest("error report endpoint rejects requests whose origin header does not match the dev server", { files: { "index.html": emptyHtmlFile({ scripts: ["/script.ts"], body: "

Origin Check

", }), "script.ts": ` console.log("hello"); `, }, async test(dev) { function u32(n: number) { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b; } function str32(s: string) { const bytes = Buffer.from(s, "utf8"); return Buffer.concat([u32(bytes.length), bytes]); } const body = Buffer.concat([str32("Error"), str32("origin-check-message"), str32(dev.baseUrl + "/"), u32(0)]); const crossOrigin = await dev.fetch("/_bun/report_error", { method: "POST", headers: { Origin: "http://other-page.example" }, body, }); expect(await crossOrigin.text()).toBe("Blocked: Origin header does not match the dev server"); expect(crossOrigin.status).toBe(403); const sameOrigin = await dev.fetch("/_bun/report_error", { method: "POST", headers: { Origin: dev.baseUrl }, body, }); expect(sameOrigin.status).toBe(200); await dev.fetch("/").expect.toInclude("

Origin Check

"); }, }); devTest("error report endpoint blanks stray non-text bytes in reported frames", { files: { "index.html": emptyHtmlFile({ scripts: ["/script.ts"], body: "

Frame Bytes

", }), "script.ts": ` console.log("hello"); `, }, async test(dev) { function u32(n: number) { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b; } function i32(n: number) { const b = Buffer.alloc(4); b.writeInt32LE(n, 0); return b; } function bytes32(bytes: Buffer) { return Buffer.concat([u32(bytes.length), bytes]); } function str32(s: string) { return bytes32(Buffer.from(s, "utf8")); } const functionName = Buffer.concat([Buffer.from("fnstart"), Buffer.from([0x9b]), Buffer.from("fnend")]); const body = Buffer.concat([ str32("Error"), str32("frame-bytes-message"), str32(dev.baseUrl + "/"), u32(1), i32(1), i32(1), bytes32(functionName), str32("foo.ts"), ]); const res = await dev.fetch("/_bun/report_error", { method: "POST", body }); const reply = Buffer.from(await res.arrayBuffer()); expect(reply.includes(Buffer.from("fnstart fnend", "latin1"))).toBe(true); expect(reply.includes(0x9b)).toBe(false); expect(res.status).toBe(200); await dev.fetch("/").expect.toInclude("

Frame Bytes

"); }, });