first commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { HMR_CLIENT_JS } from "../src/runtime.ts";
|
||||
|
||||
test("HMR client syncs fresh HTML over the websocket", () => {
|
||||
expect(HMR_CLIENT_JS).toContain('send({ type: "sync"');
|
||||
expect(HMR_CLIENT_JS).toContain('msg.type === "html"');
|
||||
expect(HMR_CLIENT_JS).toContain("applyHtml(msg.html)");
|
||||
expect(HMR_CLIENT_JS).not.toContain("fetch(location.href");
|
||||
expect(HMR_CLIENT_JS).not.toContain("location.reload()");
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { MOBILE_CLIENT } from "../src/runtime.ts";
|
||||
|
||||
test("native client is valid JavaScript and relays platform-specific event types", () => {
|
||||
expect(() => new Bun.Transpiler({ loader: "js" }).transformSync(MOBILE_CLIENT)).not.toThrow();
|
||||
expect(MOBILE_CLIENT).toContain("installNativeEvents(document)");
|
||||
expect(MOBILE_CLIENT).toContain("/^data-on-wrnexus-(?:browser|mobile)-(.+)$/");
|
||||
expect(MOBILE_CLIENT).toContain("document.addEventListener(type, handleNativeEvent, true)");
|
||||
expect(MOBILE_CLIENT).toContain('"wrnexus-" + nativeTarget + "-" + event.type');
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { toRequest, writeResponse, nodeListener } from "../src/adapters/node.ts";
|
||||
|
||||
function mockReq(opts: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
}): any {
|
||||
const em = new EventEmitter() as any;
|
||||
em.method = opts.method ?? "GET";
|
||||
em.url = opts.url ?? "/";
|
||||
em.headers = opts.headers ?? { host: "localhost" };
|
||||
setTimeout(() => {
|
||||
if (opts.body != null) em.emit("data", Buffer.from(opts.body));
|
||||
em.emit("end");
|
||||
}, 0);
|
||||
return em;
|
||||
}
|
||||
|
||||
function mockRes(): any {
|
||||
const chunks: Buffer[] = [];
|
||||
return {
|
||||
statusCode: 0,
|
||||
headers: null as any,
|
||||
headersSent: false,
|
||||
ended: false,
|
||||
writeHead(status: number, headers: any) {
|
||||
this.statusCode = status;
|
||||
this.headers = headers;
|
||||
this.headersSent = true;
|
||||
},
|
||||
write(chunk: any) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
},
|
||||
end(chunk?: any) {
|
||||
if (chunk) chunks.push(Buffer.from(chunk));
|
||||
this.ended = true;
|
||||
},
|
||||
body() {
|
||||
return Buffer.concat(chunks).toString();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("toRequest converts method, URL, headers and body", async () => {
|
||||
const req = mockReq({
|
||||
method: "POST",
|
||||
url: "/api/x?a=1",
|
||||
headers: { host: "ex.test", "content-type": "application/json" },
|
||||
body: '{"k":1}',
|
||||
});
|
||||
const r = await toRequest(req);
|
||||
expect(r.method).toBe("POST");
|
||||
expect(r.url).toBe("http://ex.test/api/x?a=1");
|
||||
expect(r.headers.get("content-type")).toBe("application/json");
|
||||
expect(await r.json()).toEqual({ k: 1 });
|
||||
});
|
||||
|
||||
test("toRequest honours x-forwarded-proto for the origin", async () => {
|
||||
const req = mockReq({ url: "/p", headers: { host: "ex.test", "x-forwarded-proto": "https" } });
|
||||
const r = await toRequest(req);
|
||||
expect(r.url).toBe("https://ex.test/p");
|
||||
});
|
||||
|
||||
test("writeResponse writes status, body and keeps multiple Set-Cookie separate", async () => {
|
||||
const res = mockRes();
|
||||
const response = new Response("hello", {
|
||||
status: 201,
|
||||
headers: { "content-type": "text/plain" },
|
||||
});
|
||||
response.headers.append("set-cookie", "a=1");
|
||||
response.headers.append("set-cookie", "b=2");
|
||||
await writeResponse(res, response);
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.headers["content-type"]).toBe("text/plain");
|
||||
expect(res.headers["set-cookie"]).toEqual(["a=1", "b=2"]);
|
||||
expect(res.body()).toBe("hello");
|
||||
});
|
||||
|
||||
test("nodeListener dispatches a request through a fetch handler", async () => {
|
||||
const handler = (req: Request) => Response.json({ path: new URL(req.url).pathname });
|
||||
const res = mockRes();
|
||||
await nodeListener(handler)(mockReq({ url: "/hi", headers: { host: "x" } }), res);
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(JSON.parse(res.body())).toEqual({ path: "/hi" });
|
||||
});
|
||||
|
||||
test("nodeListener returns 500 when the handler throws", async () => {
|
||||
const handler = () => {
|
||||
throw new Error("boom");
|
||||
};
|
||||
const res = mockRes();
|
||||
await nodeListener(handler)(mockReq({ url: "/x", headers: { host: "x" } }), res);
|
||||
expect(res.statusCode).toBe(500);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { parseComponentProps, resolveTProps } from "../src/runtime.ts";
|
||||
|
||||
test("parseComponentProps extracts quoted attrs, skips data-component", () => {
|
||||
const props = parseComponentProps('data-component="badge" label="New" count="3"');
|
||||
expect(props).toEqual({ label: "New", count: "3" });
|
||||
});
|
||||
|
||||
test("resolveTProps translates {t:key} markers via the active language", () => {
|
||||
const t = (key: string) => ({ "status.new": "Nuevo", greeting: "Hola" })[key] ?? key;
|
||||
const props = resolveTProps({ label: "{t:status.new}", plain: "as-is", hi: "{t:greeting}!" }, t);
|
||||
expect(props).toEqual({ label: "Nuevo", plain: "as-is", hi: "Hola!" });
|
||||
});
|
||||
|
||||
test("resolveTProps leaves values without markers untouched", () => {
|
||||
const props = resolveTProps({ a: "1", b: "hello" }, (k) => `T(${k})`);
|
||||
expect(props).toEqual({ a: "1", b: "hello" });
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { servePublicAsset } from "../src/public.ts";
|
||||
|
||||
test("production HTML revalidates while fingerprinted assets are immutable", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-public-"));
|
||||
mkdirSync(join(root, "assets"));
|
||||
writeFileSync(join(root, "index.html"), "home");
|
||||
writeFileSync(join(root, "assets", "app.abcdef1234.js"), "app");
|
||||
const html = await servePublicAsset(root, "/index.html", "production");
|
||||
const asset = await servePublicAsset(root, "/assets/app.abcdef1234.js", "production");
|
||||
expect(html?.headers.get("cache-control")).toBe("public, max-age=0, must-revalidate");
|
||||
expect(asset?.headers.get("cache-control")).toBe("public, max-age=31536000, immutable");
|
||||
expect(asset?.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
});
|
||||
|
||||
test("public server rejects encoded traversal", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-public-"));
|
||||
expect(await servePublicAsset(root, "/%2e%2e/secret", "production")).toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user