Update tests

This commit is contained in:
Benjamin Toby 2026-03-21 10:20:16 +01:00
parent d2ddaef0d4
commit 3545b6dc08
37 changed files with 7 additions and 862 deletions

View File

@ -1 +0,0 @@
export {};

View File

@ -1,52 +0,0 @@
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");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,37 +0,0 @@
import { describe, it, expect } from "bun:test";
import grabCacheNames from "../../../functions/cache/grab-cache-names";
describe("grabCacheNames", () => {
it("returns cache_name and cache_meta_name for a simple key", () => {
const { cache_name, cache_meta_name } = grabCacheNames({ key: "home" });
expect(cache_name).toBe("home.res.html");
expect(cache_meta_name).toBe("home.meta.json");
});
it("defaults paradigm to html", () => {
const { cache_name } = grabCacheNames({ key: "page" });
expect(cache_name).toEndWith(".res.html");
});
it("uses json paradigm when specified", () => {
const { cache_name } = grabCacheNames({ key: "api-data", paradigm: "json" });
expect(cache_name).toBe("api-data.res.json");
});
it("URL-encodes the key", () => {
const { cache_name, cache_meta_name } = grabCacheNames({
key: "/blog/hello world",
});
const encoded = encodeURIComponent("/blog/hello world");
expect(cache_name).toBe(`${encoded}.res.html`);
expect(cache_meta_name).toBe(`${encoded}.meta.json`);
});
it("handles keys with special characters", () => {
const key = "page?id=1&sort=asc";
const { cache_name } = grabCacheNames({ key });
expect(cache_name).toBe(`${encodeURIComponent(key)}.res.html`);
});
it("cache_meta_name always uses .meta.json regardless of paradigm", () => {
const { cache_meta_name } = grabCacheNames({
key: "test",
paradigm: "json",
});
expect(cache_meta_name).toBe("test.meta.json");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,73 +0,0 @@
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");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,97 +0,0 @@
import { describe, it, expect } from "bun:test";
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
describe("grabWebMetaHTML", () => {
it("returns empty string for empty meta object", () => {
expect(grabWebMetaHTML({ meta: {} })).toBe("");
});
it("generates a title tag", () => {
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
expect(html).toContain("<title>My Page</title>");
});
it("generates a description meta tag", () => {
const html = grabWebMetaHTML({ meta: { description: "A description" } });
expect(html).toContain('<meta name="description" content="A description"');
});
it("joins array keywords with comma", () => {
const html = grabWebMetaHTML({
meta: { keywords: ["react", "bun", "ssr"] },
});
expect(html).toContain('content="react, bun, ssr"');
});
it("uses string keywords directly", () => {
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
expect(html).toContain('content="react, bun"');
});
it("generates author meta tag", () => {
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
expect(html).toContain('<meta name="author" content="Alice"');
});
it("generates robots meta tag", () => {
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
expect(html).toContain('<meta name="robots" content="noindex"');
});
it("generates canonical link tag", () => {
const html = grabWebMetaHTML({
meta: { canonical: "https://example.com/page" },
});
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
});
it("generates theme-color meta tag", () => {
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
});
it("generates OG tags", () => {
const html = grabWebMetaHTML({
meta: {
og: {
title: "OG Title",
description: "OG Desc",
image: "https://example.com/img.png",
url: "https://example.com",
type: "website",
siteName: "Example",
locale: "en_US",
},
},
});
expect(html).toContain('<meta property="og:title" content="OG Title"');
expect(html).toContain('<meta property="og:description" content="OG Desc"');
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
expect(html).toContain('<meta property="og:url" content="https://example.com"');
expect(html).toContain('<meta property="og:type" content="website"');
expect(html).toContain('<meta property="og:site_name" content="Example"');
expect(html).toContain('<meta property="og:locale" content="en_US"');
});
it("generates Twitter card tags", () => {
const html = grabWebMetaHTML({
meta: {
twitter: {
card: "summary_large_image",
title: "Tweet Title",
description: "Tweet Desc",
image: "https://example.com/tw.png",
site: "@example",
creator: "@alice",
},
},
});
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
expect(html).toContain('<meta name="twitter:site" content="@example"');
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
});
it("skips undefined OG fields", () => {
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
expect(html).toContain("og:title");
expect(html).not.toContain("og:description");
expect(html).not.toContain("og:image");
});
it("does not emit tags for missing fields", () => {
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
expect(html).not.toContain("description");
expect(html).not.toContain("og:");
expect(html).not.toContain("twitter:");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,37 +0,0 @@
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");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,65 +0,0 @@
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);
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,36 +0,0 @@
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);
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,62 +0,0 @@
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");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,33 +0,0 @@
import { describe, it, expect } from "bun:test";
import deserializeQuery from "../../utils/deserialize-query";
describe("deserializeQuery", () => {
it("passes through a plain object unchanged", () => {
const input = { foo: "bar" };
expect(deserializeQuery(input)).toEqual({ foo: "bar" });
});
it("parses a JSON string into an object", () => {
const input = JSON.stringify({ a: 1, b: "hello" });
expect(deserializeQuery(input)).toEqual({ a: 1, b: "hello" });
});
it("deep-parses string values that look like JSON objects", () => {
const nested = { filter: JSON.stringify({ status: "active" }) };
const result = deserializeQuery(nested);
expect(result.filter).toEqual({ status: "active" });
});
it("deep-parses string values that look like JSON arrays", () => {
const nested = { ids: JSON.stringify([1, 2, 3]) };
const result = deserializeQuery(nested);
expect(result.ids).toEqual([1, 2, 3]);
});
it("leaves plain string values alone", () => {
const input = { name: "alice", age: "30" };
expect(deserializeQuery(input)).toEqual({ name: "alice", age: "30" });
});
it("returns an empty object for an empty JSON string", () => {
expect(deserializeQuery("{}")).toEqual({});
});
it("returns an empty object for an invalid JSON string", () => {
// EJSON.parse returns undefined → Object(undefined) → {}
expect(deserializeQuery("not-json")).toEqual({});
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,52 +0,0 @@
import { describe, it, expect } from "bun:test";
import EJSON from "../../utils/ejson";
describe("EJSON.parse", () => {
it("parses a valid JSON string", () => {
expect(EJSON.parse('{"a":1}')).toEqual({ a: 1 });
});
it("parses a JSON array string", () => {
expect(EJSON.parse('[1,2,3]')).toEqual([1, 2, 3]);
});
it("returns undefined for null input", () => {
expect(EJSON.parse(null)).toBeUndefined();
});
it("returns undefined for empty string", () => {
expect(EJSON.parse("")).toBeUndefined();
});
it("returns undefined for invalid JSON", () => {
expect(EJSON.parse("{bad json")).toBeUndefined();
});
it("returns the object directly when passed an object (typeof object)", () => {
const obj = { x: 1 };
expect(EJSON.parse(obj)).toBe(obj);
});
it("returns undefined for a number input", () => {
expect(EJSON.parse(42)).toBeUndefined();
});
it("applies a reviver function", () => {
const result = EJSON.parse('{"a":"2"}', (key, value) => key === "a" ? Number(value) : value);
expect(result).toEqual({ a: 2 });
});
});
describe("EJSON.stringify", () => {
it("stringifies an object", () => {
expect(EJSON.stringify({ a: 1 })).toBe('{"a":1}');
});
it("stringifies an array", () => {
expect(EJSON.stringify([1, 2, 3])).toBe("[1,2,3]");
});
it("applies spacing", () => {
expect(EJSON.stringify({ a: 1 }, null, 2)).toBe('{\n "a": 1\n}');
});
it("returns undefined for circular references", () => {
const obj = {};
obj.self = obj;
expect(EJSON.stringify(obj)).toBeUndefined();
});
it("stringifies null", () => {
expect(EJSON.stringify(null)).toBe("null");
});
it("stringifies a number", () => {
expect(EJSON.stringify(42)).toBe("42");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,23 +0,0 @@
import { describe, it, expect } from "bun:test";
import AppNames from "../../utils/grab-app-names";
describe("AppNames", () => {
it("has a defaultPort of 7000", () => {
expect(AppNames.defaultPort).toBe(7000);
});
it("has the correct defaultAssetPrefix", () => {
expect(AppNames.defaultAssetPrefix).toBe("_bunext/static");
});
it("has name Bunext", () => {
expect(AppNames.name).toBe("Bunext");
});
it("has a version string", () => {
expect(typeof AppNames.version).toBe("string");
expect(AppNames.version.length).toBeGreaterThan(0);
});
it("has defaultDistDir as .bunext", () => {
expect(AppNames.defaultDistDir).toBe(".bunext");
});
it("has RootPagesComponentName as __root", () => {
expect(AppNames.RootPagesComponentName).toBe("__root");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,38 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import grabAppPort from "../../utils/grab-app-port";
const originalEnv = process.env.PORT;
beforeEach(() => {
delete process.env.PORT;
global.CONFIG = {};
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env.PORT = originalEnv;
}
else {
delete process.env.PORT;
}
});
describe("grabAppPort", () => {
it("returns the default port (7000) when no config or env set", () => {
expect(grabAppPort()).toBe(7000);
});
it("uses PORT env variable when set", () => {
process.env.PORT = "8080";
expect(grabAppPort()).toBe(8080);
});
it("uses config.port when PORT env is not set", () => {
global.CONFIG = { port: 3000 };
expect(grabAppPort()).toBe(3000);
});
it("PORT env takes precedence over config.port", () => {
process.env.PORT = "9000";
global.CONFIG = { port: 3000 };
expect(grabAppPort()).toBe(9000);
});
it("handles non-numeric PORT env gracefully via numberfy", () => {
process.env.PORT = "abc";
// numberfy strips non-numeric chars, "abc" → "" → 0
expect(grabAppPort()).toBe(0);
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,30 +0,0 @@
import { describe, it, expect, beforeEach } from "bun:test";
import grabConstants from "../../utils/grab-constants";
beforeEach(() => {
global.CONFIG = {};
});
describe("grabConstants", () => {
it("has the correct ClientRootElementIDName", () => {
expect(grabConstants().ClientRootElementIDName).toBe("__bunext");
});
it("has the correct ClientWindowPagePropsName", () => {
expect(grabConstants().ClientWindowPagePropsName).toBe("__PAGE_PROPS__");
});
it("has the correct ClientRootComponentWindowName", () => {
expect(grabConstants().ClientRootComponentWindowName).toBe("BUNEXT_ROOT");
});
it("calculates MBInBytes as 1024 * 1024", () => {
expect(grabConstants().MBInBytes).toBe(1024 * 1024);
});
it("ServerDefaultRequestBodyLimitBytes is 10 MB", () => {
expect(grabConstants().ServerDefaultRequestBodyLimitBytes).toBe(10 * 1024 * 1024);
});
it("MaxBundlerRebuilds is 5", () => {
expect(grabConstants().MaxBundlerRebuilds).toBe(5);
});
it("returns the current global.CONFIG", () => {
const cfg = { port: 9000 };
global.CONFIG = cfg;
expect(grabConstants().config).toBe(cfg);
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,55 +0,0 @@
import { describe, it, expect } from "bun:test";
import path from "path";
import grabDirNames from "../../utils/grab-dir-names";
describe("grabDirNames", () => {
it("derives all paths from process.cwd()", () => {
const cwd = process.cwd();
const dirs = grabDirNames();
expect(dirs.ROOT_DIR).toBe(cwd);
expect(dirs.SRC_DIR).toBe(path.join(cwd, "src"));
expect(dirs.PAGES_DIR).toBe(path.join(cwd, "src", "pages"));
expect(dirs.API_DIR).toBe(path.join(cwd, "src", "pages", "api"));
expect(dirs.PUBLIC_DIR).toBe(path.join(cwd, "public"));
});
it("nests HYDRATION_DST_DIR under public/__bunext/pages", () => {
const dirs = grabDirNames();
expect(dirs.HYDRATION_DST_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "pages"));
});
it("nests BUNEXT_CACHE_DIR under public/__bunext/cache", () => {
const dirs = grabDirNames();
expect(dirs.BUNEXT_CACHE_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "cache"));
});
it("places map JSON file inside HYDRATION_DST_DIR", () => {
const dirs = grabDirNames();
expect(dirs.HYDRATION_DST_DIR_MAP_JSON_FILE).toBe(path.join(dirs.HYDRATION_DST_DIR, "map.json"));
});
it("places CONFIG_FILE at root", () => {
const dirs = grabDirNames();
expect(dirs.CONFIG_FILE).toBe(path.join(dirs.ROOT_DIR, "bunext.config.ts"));
});
it("places BUNX_TMP_DIR inside .bunext", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_TMP_DIR).toContain(".bunext");
expect(dirs.BUNX_TMP_DIR).toEndWith(".tmp");
});
it("places BUNX_HYDRATION_SRC_DIR under client/hydration-src", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_HYDRATION_SRC_DIR).toContain(path.join("client", "hydration-src"));
});
it("sets 404 file name to not-found", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_ROOT_404_FILE_NAME).toBe("not-found");
});
it("sets 500 file name to server-error", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_ROOT_500_FILE_NAME).toBe("server-error");
});
it("preset 404 component path ends with not-found.tsx", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_ROOT_404_PRESET_COMPONENT).toEndWith("not-found.tsx");
});
it("preset 500 component path ends with server-error.tsx", () => {
const dirs = grabDirNames();
expect(dirs.BUNX_ROOT_500_PRESET_COMPONENT).toEndWith("server-error.tsx");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,32 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import grabOrigin from "../../utils/grab-origin";
const originalPort = process.env.PORT;
beforeEach(() => {
delete process.env.PORT;
global.CONFIG = {};
});
afterEach(() => {
if (originalPort !== undefined) {
process.env.PORT = originalPort;
}
else {
delete process.env.PORT;
}
});
describe("grabOrigin", () => {
it("returns config.origin when set", () => {
global.CONFIG = { origin: "https://example.com" };
expect(grabOrigin()).toBe("https://example.com");
});
it("falls back to http://localhost:<port> using default port", () => {
expect(grabOrigin()).toBe("http://localhost:7000");
});
it("falls back using PORT env variable", () => {
process.env.PORT = "8080";
expect(grabOrigin()).toBe("http://localhost:8080");
});
it("falls back using config.port", () => {
global.CONFIG = { port: 3700 };
expect(grabOrigin()).toBe("http://localhost:3700");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,46 +0,0 @@
import { describe, it, expect } from "bun:test";
import grabPageName from "../../utils/grab-page-name";
describe("grabPageName", () => {
it("returns the page name for a simple page path", () => {
expect(grabPageName({ path: "/home/user/project/src/pages/about.tsx" }))
.toBe("about");
});
it("returns 'index' for a root index file (no -index stripping at root)", () => {
// -index suffix is only stripped when joined: e.g. "blog-index" → "blog"
// A standalone "index" filename has no leading dash so stays as-is
expect(grabPageName({ path: "/home/user/project/src/pages/index.tsx" }))
.toBe("index");
});
it("handles nested page paths", () => {
expect(grabPageName({ path: "/home/user/project/src/pages/blog/post.tsx" })).toBe("blog-post");
});
it("strips -index suffix from nested index files", () => {
expect(grabPageName({ path: "/home/user/project/src/pages/blog/index.tsx" })).toBe("blog");
});
it("converts dynamic segments [slug] by replacing brackets", () => {
const result = grabPageName({
path: "/home/user/project/src/pages/blog/[slug].tsx",
});
// [ → - and ] is dropped (not a-z or -), so [slug] → -slug
expect(result).toBe("blog--slug");
});
it("converts spread [...params] segments", () => {
const result = grabPageName({
path: "/home/user/project/src/pages/[...params].tsx",
});
// "[...params]" → remove ext → "[...params]"
// [ → "-" → "-...params]"
// "..." → "-" → "--params]"
// strip non [a-z-] → "--params"
expect(result).toBe("--params");
});
it("strips uppercase letters (only a-z and - are kept)", () => {
// [^a-z\-] strips uppercase — 'A' is removed, 'bout' remains
expect(grabPageName({ path: "/home/user/project/src/pages/About.tsx" })).toBe("bout");
});
it("handles deeply nested paths", () => {
expect(grabPageName({
path: "/home/user/project/src/pages/admin/users/list.tsx",
})).toBe("admin-users-list");
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,32 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import isDevelopment from "../../utils/is-development";
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
// Reset global config before each test
global.CONFIG = {};
});
afterEach(() => {
process.env.NODE_ENV = originalEnv;
});
describe("isDevelopment", () => {
it("returns false when NODE_ENV is production", () => {
process.env.NODE_ENV = "production";
global.CONFIG = { development: true };
expect(isDevelopment()).toBe(false);
});
it("returns true when config.development is true and NODE_ENV is not production", () => {
process.env.NODE_ENV = "development";
global.CONFIG = { development: true };
expect(isDevelopment()).toBe(true);
});
it("returns false when config.development is false", () => {
process.env.NODE_ENV = "development";
global.CONFIG = { development: false };
expect(isDevelopment()).toBe(false);
});
it("returns false when config.development is undefined", () => {
process.env.NODE_ENV = "development";
global.CONFIG = {};
expect(isDevelopment()).toBe(false);
});
});

View File

@ -1 +0,0 @@
export {};

View File

@ -1,43 +0,0 @@
import { describe, it, expect } from "bun:test";
import numberfy, { _n } from "../../utils/numberfy";
describe("numberfy", () => {
it("converts a plain integer string", () => {
expect(numberfy("42")).toBe(42);
});
it("converts a float string preserving decimals", () => {
expect(numberfy("3.14")).toBe(3.14);
});
it("strips non-numeric characters", () => {
expect(numberfy("$1,234.56")).toBe(1234.56);
});
it("returns 0 for an empty string", () => {
expect(numberfy("")).toBe(0);
});
it("returns 0 for undefined", () => {
expect(numberfy(undefined)).toBe(0);
});
it("returns 0 for null", () => {
expect(numberfy(null)).toBe(0);
});
it("passes through a number directly", () => {
expect(numberfy(7)).toBe(7);
});
it("rounds when no decimals specified and no decimal in input", () => {
expect(numberfy("5.0")).toBe(5);
});
it("respects decimals=0 by rounding", () => {
expect(numberfy("3.7", 0)).toBe(4);
});
it("respects explicit decimals parameter", () => {
expect(numberfy("3.14159", 2)).toBe(3.14);
});
it("preserves existing decimal places when no decimals arg given", () => {
expect(numberfy("1.500")).toBe(1.5);
});
it("strips a trailing dot", () => {
expect(numberfy("5.")).toBe(5);
});
it("_n alias works identically", () => {
expect(_n("10")).toBe(10);
});
});

View File

@ -19,5 +19,11 @@
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
"exclude": [
"node_modules",
"dist",
"src/__tests__",
"**/__tests__",
"__tests__"
]
}