Update tests
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
||||
import startServer from "../../../src/functions/server/start-server";
|
||||
import bunextInit from "../../../src/functions/bunext-init";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
let originalCwd = process.cwd();
|
||||
describe("E2E Integration", () => {
|
||||
let server;
|
||||
beforeAll(async () => {
|
||||
// Change to the fixture directory to simulate actual user repo
|
||||
const fixtureDir = path.resolve(__dirname, "../__fixtures__/app");
|
||||
process.chdir(fixtureDir);
|
||||
// Mock grabAppPort to assign dynamically to avoid port conflicts
|
||||
global.CONFIG = { development: true };
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (server) {
|
||||
server.stop(true);
|
||||
}
|
||||
process.chdir(originalCwd);
|
||||
// Ensure to remove the dummy generated .bunext folder
|
||||
const dotBunext = path.resolve(__dirname, "../__fixtures__/app/.bunext");
|
||||
if (fs.existsSync(dotBunext)) {
|
||||
fs.rmSync(dotBunext, { recursive: true, force: true });
|
||||
}
|
||||
const pubBunext = path.resolve(__dirname, "../__fixtures__/app/public/__bunext");
|
||||
if (fs.existsSync(pubBunext)) {
|
||||
fs.rmSync(pubBunext, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
test("boots up the server and correctly routes to index.tsx page", async () => {
|
||||
// Mock to randomize port
|
||||
// Note: Bun test runs modules in isolation but startServer imports grab-app-port
|
||||
// If we can't easily mock we can set PORT env
|
||||
process.env.PORT = "0"; // Let Bun.serve pick port
|
||||
await bunextInit();
|
||||
server = await startServer();
|
||||
expect(server).toBeDefined();
|
||||
// Fetch the index page
|
||||
const response = await fetch(`http://localhost:${server.port}/`);
|
||||
expect(response.status).toBe(200);
|
||||
const html = await response.text();
|
||||
expect(html).toContain("Hello E2E");
|
||||
});
|
||||
test("returns 404 for unknown route", async () => {
|
||||
const response = await fetch(`http://localhost:${server.port}/unknown-foo-bar123`);
|
||||
expect(response.status).toBe(404);
|
||||
const text = await response.text();
|
||||
// Assume default 404 preset component is rendered
|
||||
expect(text).toContain("404");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test, mock, afterAll } from "bun:test";
|
||||
import bunextRequestHandler from "../../../../src/functions/server/bunext-req-handler";
|
||||
mock.module("../../../../src/utils/is-development", () => ({
|
||||
default: () => true
|
||||
}));
|
||||
mock.module("../../../../src/utils/grab-constants", () => ({
|
||||
default: () => ({
|
||||
config: {
|
||||
middleware: async ({ url }) => {
|
||||
if (url.pathname === "/blocked") {
|
||||
return new Response("Blocked by middleware", { status: 403 });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
})
|
||||
}));
|
||||
mock.module("../../../../src/functions/server/handle-routes", () => ({
|
||||
default: async () => new Response("api-routes")
|
||||
}));
|
||||
mock.module("../../../../src/functions/server/handle-public", () => ({
|
||||
default: async () => new Response("public")
|
||||
}));
|
||||
mock.module("../../../../src/functions/server/handle-files", () => ({
|
||||
default: async () => new Response("files")
|
||||
}));
|
||||
mock.module("../../../../src/functions/server/web-pages/handle-web-pages", () => ({
|
||||
default: async () => new Response("web-pages")
|
||||
}));
|
||||
/**
|
||||
* Tests for the `bunext-req-handler` module.
|
||||
* Ensures that requests are correctly routed to the proper subsystem.
|
||||
*/
|
||||
describe("bunext-req-handler", () => {
|
||||
afterAll(() => {
|
||||
mock.restore();
|
||||
});
|
||||
test("middleware is caught", async () => {
|
||||
const req = new Request("http://localhost/blocked");
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(res.status).toBe(403);
|
||||
expect(await res.text()).toBe("Blocked by middleware");
|
||||
});
|
||||
test("routes /__hmr to handleHmr in dev", async () => {
|
||||
global.ROUTER = { match: () => ({}) };
|
||||
global.HMR_CONTROLLERS = [];
|
||||
const req = new Request("http://localhost/__hmr", {
|
||||
headers: { referer: "http://localhost/" }
|
||||
});
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
||||
});
|
||||
test("routes /api/ to handleRoutes", async () => {
|
||||
const req = new Request("http://localhost/api/users");
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(await res.text()).toBe("api-routes");
|
||||
});
|
||||
test("routes /public/ to handlePublic", async () => {
|
||||
const req = new Request("http://localhost/public/image.png");
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(await res.text()).toBe("public");
|
||||
});
|
||||
test("routes files like .js to handleFiles", async () => {
|
||||
const req = new Request("http://localhost/script.js");
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(await res.text()).toBe("files");
|
||||
});
|
||||
test("routes anything else to handleWebPages", async () => {
|
||||
const req = new Request("http://localhost/about");
|
||||
const res = await bunextRequestHandler({ req });
|
||||
expect(await res.text()).toBe("web-pages");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test";
|
||||
import handleHmr from "../../../../src/functions/server/handle-hmr";
|
||||
describe("handle-hmr", () => {
|
||||
beforeEach(() => {
|
||||
global.ROUTER = {
|
||||
match: (path) => {
|
||||
if (path === "/test")
|
||||
return { filePath: "/test-file" };
|
||||
return null;
|
||||
}
|
||||
};
|
||||
global.HMR_CONTROLLERS = [];
|
||||
global.BUNDLER_CTX_MAP = [
|
||||
{ local_path: "/test-file" }
|
||||
];
|
||||
});
|
||||
afterEach(() => {
|
||||
global.ROUTER = undefined;
|
||||
global.HMR_CONTROLLERS = [];
|
||||
global.BUNDLER_CTX_MAP = undefined;
|
||||
});
|
||||
test("sets up SSE stream and pushes to HMR_CONTROLLERS", async () => {
|
||||
const req = new Request("http://localhost/hmr", {
|
||||
headers: {
|
||||
"referer": "http://localhost/test"
|
||||
}
|
||||
});
|
||||
const res = await handleHmr({ req });
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
|
||||
expect(res.headers.get("Connection")).toBe("keep-alive");
|
||||
expect(global.HMR_CONTROLLERS.length).toBe(1);
|
||||
const controller = global.HMR_CONTROLLERS[0];
|
||||
expect(controller.page_url).toBe("http://localhost/test");
|
||||
expect(controller.target_map?.local_path).toBe("/test-file");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, test, mock, afterAll } from "bun:test";
|
||||
import handleRoutes from "../../../../src/functions/server/handle-routes";
|
||||
mock.module("../../../../src/utils/is-development", () => ({
|
||||
default: () => false
|
||||
}));
|
||||
mock.module("../../../../src/utils/grab-constants", () => ({
|
||||
default: () => ({ MBInBytes: 1048576, ServerDefaultRequestBodyLimitBytes: 5242880 })
|
||||
}));
|
||||
mock.module("../../../../src/utils/grab-router", () => ({
|
||||
default: () => ({
|
||||
match: (path) => {
|
||||
if (path === "/api/test")
|
||||
return { filePath: "/test-path" };
|
||||
if (path === "/api/large")
|
||||
return { filePath: "/large-path" };
|
||||
return null;
|
||||
}
|
||||
})
|
||||
}));
|
||||
mock.module("../../../../src/utils/grab-route-params", () => ({
|
||||
default: async () => ({ params: {}, searchParams: {} })
|
||||
}));
|
||||
mock.module("/test-path", () => ({
|
||||
default: async () => new Response("OK", { status: 200 })
|
||||
}));
|
||||
mock.module("/large-path", () => ({
|
||||
default: async () => new Response("Large OK", { status: 200 }),
|
||||
config: { maxRequestBodyMB: 1 }
|
||||
}));
|
||||
/**
|
||||
* Tests for routing logic within `handle-routes`.
|
||||
*/
|
||||
describe("handle-routes", () => {
|
||||
afterAll(() => {
|
||||
mock.restore();
|
||||
});
|
||||
test("returns 401 for unknown route", async () => {
|
||||
const req = new Request("http://localhost/api/unknown");
|
||||
const res = await handleRoutes({ req });
|
||||
expect(res.status).toBe(401);
|
||||
const json = await res.json();
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.msg).toContain("not found");
|
||||
});
|
||||
test("calls matched module default export", async () => {
|
||||
const req = new Request("http://localhost/api/test");
|
||||
const res = await handleRoutes({ req });
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("OK");
|
||||
});
|
||||
test("enforces request body size limits", async () => {
|
||||
// limit is 1MB from mock config
|
||||
const req = new Request("http://localhost/api/large", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-length": "2000000" // ~2MB
|
||||
},
|
||||
body: "x".repeat(10) // the actual body doesn't matter since handleRoutes only checks the header
|
||||
});
|
||||
const res = await handleRoutes({ req });
|
||||
expect(res.status).toBe(413);
|
||||
const json = await res.json();
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test, mock, afterEach } from "bun:test";
|
||||
import startServer from "../../../../src/functions/server/start-server";
|
||||
import { log } from "../../../../src/utils/log";
|
||||
// Mock log so we don't spam terminal during tests
|
||||
mock.module("../../../../src/utils/log", () => ({
|
||||
log: {
|
||||
server: mock((msg) => { }),
|
||||
info: mock((msg) => { }),
|
||||
error: mock((msg) => { }),
|
||||
}
|
||||
}));
|
||||
// Mock grabConfig so it doesn't try to look for bunext.config.ts and exit process
|
||||
mock.module("../../../../src/functions/grab-config", () => ({
|
||||
default: async () => ({})
|
||||
}));
|
||||
// Mock grabAppPort to return 0 so Bun.serve picks a random port
|
||||
mock.module("../../../../src/utils/grab-app-port", () => ({
|
||||
default: () => 0
|
||||
}));
|
||||
describe("startServer", () => {
|
||||
afterEach(() => {
|
||||
if (global.SERVER) {
|
||||
global.SERVER.stop(true);
|
||||
global.SERVER = undefined;
|
||||
}
|
||||
});
|
||||
test("starts the server and assigns to global.SERVER", async () => {
|
||||
global.CONFIG = { development: true };
|
||||
const server = await startServer();
|
||||
expect(server).toBeDefined();
|
||||
expect(server.port).toBeGreaterThan(0);
|
||||
expect(global.SERVER).toBe(server);
|
||||
expect(log.server).toHaveBeenCalled();
|
||||
server.stop(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
||||
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
||||
import React, { useState } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { GlobalWindow } from "happy-dom";
|
||||
// A mock application component to test hydration
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
return (_jsxs("div", { id: "app-root", children: [_jsx("h1", { children: "Test Hydration" }), _jsxs("p", { "data-testid": "count", children: ["Count: ", count] }), _jsx("button", { "data-testid": "btn", onClick: () => setCount(c => c + 1), children: "Increment" })] }));
|
||||
}
|
||||
describe("React Hydration", () => {
|
||||
let window;
|
||||
let document;
|
||||
beforeEach(() => {
|
||||
window = new GlobalWindow();
|
||||
document = window.document;
|
||||
global.window = window;
|
||||
global.document = document;
|
||||
global.navigator = { userAgent: "node.js" };
|
||||
});
|
||||
afterEach(() => {
|
||||
// Clean up global mocks
|
||||
delete global.window;
|
||||
delete global.document;
|
||||
delete global.navigator;
|
||||
window.close();
|
||||
});
|
||||
test("hydrates a server-rendered component and binds events", async () => {
|
||||
// 1. Server-side render
|
||||
const html = renderToString(_jsx(App, {}));
|
||||
// 2. Setup DOM as it would be delivered to the client
|
||||
document.body.innerHTML = `<div id="root">${html}</div>`;
|
||||
const rootNode = document.getElementById("root");
|
||||
// 3. Hydrate
|
||||
let hydrateError = null;
|
||||
try {
|
||||
await new Promise((resolve) => {
|
||||
hydrateRoot(rootNode, _jsx(App, {}), {
|
||||
onRecoverableError: (err) => {
|
||||
hydrateError = err;
|
||||
}
|
||||
});
|
||||
setTimeout(resolve, 50); // let React finish hydration
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
hydrateError = e;
|
||||
}
|
||||
// Verify no hydration errors
|
||||
expect(hydrateError).toBeNull();
|
||||
// 4. Verify client-side interactivity
|
||||
const button = document.querySelector('[data-testid="btn"]');
|
||||
const countText = document.querySelector('[data-testid="count"]');
|
||||
expect(countText.textContent).toBe("Count: 0");
|
||||
// Simulate click
|
||||
button.dispatchEvent(new window.Event("click", { bubbles: true }));
|
||||
// Let async state updates process
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(countText.textContent).toBe("Count: 1");
|
||||
});
|
||||
});
|
||||
+1
-6
@@ -2,9 +2,7 @@ import handleWebPages from "./web-pages/handle-web-pages";
|
||||
import handleRoutes from "./handle-routes";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import handleHmr from "./handle-hmr";
|
||||
import handleHmrUpdate from "./handle-hmr-update";
|
||||
import handlePublic from "./handle-public";
|
||||
import handleFiles from "./handle-files";
|
||||
export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
@@ -26,10 +24,7 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
req = middleware_res;
|
||||
}
|
||||
}
|
||||
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
|
||||
response = await handleHmrUpdate({ req });
|
||||
}
|
||||
else if (url.pathname === "/__hmr" && is_dev) {
|
||||
if (url.pathname === "/__hmr" && is_dev) {
|
||||
response = await handleHmr({ req });
|
||||
}
|
||||
else if (url.pathname.startsWith("/api/")) {
|
||||
|
||||
@@ -21,6 +21,11 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
|
||||
routeParams,
|
||||
module,
|
||||
bundledMap,
|
||||
serverRes: {
|
||||
responseOptions: {
|
||||
status: is404 ? 404 : 500
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
catch {
|
||||
@@ -37,6 +42,11 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
|
||||
routeParams,
|
||||
module: { default: DefaultNotFound },
|
||||
bundledMap: {},
|
||||
serverRes: {
|
||||
responseOptions: {
|
||||
status: is404 ? 404 : 500
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user