Files
bun-src/test/bake/dev/html.test.ts
T
2026-08-27 21:09:14 +00:00

373 lines
10 KiB
TypeScript

// 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: "<h1>Hello</h1>",
}),
"script.ts": `
console.log("hello");
`,
},
async test(dev) {
await dev.fetch("/").expect.toInclude("<h1>Hello</h1>");
await dev.fetch("/").expect.toInclude("<h1>Hello</h1>");
await dev.patch("index.html", {
find: "Hello",
replace: "World",
});
await dev.fetch("/").expect.toInclude("<h1>World</h1>");
// 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("<h1>Hello</h1>");
});
await c.expectMessage("hello");
await c.expectReload(async () => {
await dev.patch("index.html", {
find: "Hello",
replace: "Bar",
});
await dev.fetch("/").expect.toInclude("<h1>Bar</h1>");
});
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": `
<!DOCTYPE html><html><head></head><body>
<img src="image.png" alt="test image">
</body></html>
`,
"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": `
<!DOCTYPE html><html><head></head><body>
<script type="module" src="script.ts"></script>
</body></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": `
<!DOCTYPE html>
<html>
<head></head>
<body>
<script type="module" src="/script.ts"></script>
</body>
</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": `
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>index | Powered by Bun</title>
<link rel="stylesheet" href="./index.css" />
<link rel="icon" type="image/x-icon" href="https://bun.sh/favicon.ico" />
</head>
<body>
<div id="root"></div>
<script src="./index.client.tsx" type="module"></script>
</body>
</html>
`,
"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 type="module" src="/script.ts"></script>
`,
"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 type="module" src="/script.ts"></script>
`,
"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: "<h1>Error Report</h1>",
}),
"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("<h1>Error Report</h1>");
},
});
devTest("error report endpoint rejects requests whose origin header does not match the dev server", {
files: {
"index.html": emptyHtmlFile({
scripts: ["/script.ts"],
body: "<h1>Origin Check</h1>",
}),
"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("<h1>Origin Check</h1>");
},
});
devTest("error report endpoint blanks stray non-text bytes in reported frames", {
files: {
"index.html": emptyHtmlFile({
scripts: ["/script.ts"],
body: "<h1>Frame Bytes</h1>",
}),
"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("<h1>Frame Bytes</h1>");
},
});