/**
* @mandujs/core/desktop — window factory tests
*
* These tests run **without actually loading `webview-bun`**: we either
* stub the optional peer via a mocked import cache or exercise option
* validation / size-hint mapping / error paths that never need the FFI
* peer at all. CI does not have WebView2 / WKWebView available, so any
* test that would open a real window is marked describe.skipIf.
*/
import { describe, it, expect, beforeEach } from "bun:test";
import {
_DEFAULTS,
_mapSizeHint,
_resetWebviewBunCache,
_validateOptions,
createWindow,
} from "../window";
import type { WindowOptions } from "../types";
describe("@mandujs/core/desktop — types & validation", () => {
it("_DEFAULTS match the documented contract", () => {
expect(_DEFAULTS.title).toBe("Mandu Desktop");
expect(_DEFAULTS.width).toBe(1024);
expect(_DEFAULTS.height).toBe(768);
expect(_DEFAULTS.hint).toBe("none");
expect(_DEFAULTS.debug).toBe(false);
});
it("_mapSizeHint translates strings to peer enum values", () => {
const fakeEnum = { NONE: 0, MIN: 1, MAX: 2, FIXED: 3 };
expect(_mapSizeHint("none", fakeEnum)).toBe(0);
expect(_mapSizeHint("min", fakeEnum)).toBe(1);
expect(_mapSizeHint("max", fakeEnum)).toBe(2);
expect(_mapSizeHint("fixed", fakeEnum)).toBe(3);
expect(_mapSizeHint(undefined, fakeEnum)).toBe(0); // default → NONE
});
it("_validateOptions rejects missing url", () => {
expect(() => _validateOptions({} as WindowOptions)).toThrow(/url/);
});
it("_validateOptions rejects non-string url", () => {
expect(() =>
_validateOptions({ url: 123 as unknown as string }),
).toThrow(/url/);
});
it("_validateOptions rejects malformed url", () => {
expect(() => _validateOptions({ url: "not a url" })).toThrow(/valid URL/);
});
it("_validateOptions rejects forbidden protocols", () => {
expect(() =>
_validateOptions({ url: "javascript:alert(1)" }),
).toThrow(/protocol/);
expect(() =>
_validateOptions({ url: "chrome://version" }),
).toThrow(/protocol/);
});
it("_validateOptions accepts http, https, file, and data URLs", () => {
expect(() =>
_validateOptions({ url: "http://127.0.0.1:3333/" }),
).not.toThrow();
expect(() =>
_validateOptions({ url: "https://example.com/" }),
).not.toThrow();
expect(() =>
_validateOptions({ url: "file:///tmp/index.html" }),
).not.toThrow();
expect(() =>
_validateOptions({ url: "data:text/html,
hi
" }),
).not.toThrow();
});
it("_validateOptions rejects non-positive width/height", () => {
expect(() =>
_validateOptions({ url: "http://x", width: 0 }),
).toThrow(/width/);
expect(() =>
_validateOptions({ url: "http://x", height: -50 }),
).toThrow(/height/);
expect(() =>
_validateOptions({
url: "http://x",
width: Number.NaN,
}),
).toThrow(/width/);
});
it("_validateOptions rejects unknown hint values", () => {
expect(() =>
_validateOptions({
url: "http://x",
hint: "resizable" as never,
}),
).toThrow(/hint/);
});
it("_validateOptions rejects non-function handlers", () => {
expect(() =>
_validateOptions({
url: "http://x",
handlers: { foo: "not a function" as unknown as () => void },
}),
).toThrow(/handlers\.foo/);
});
it("_validateOptions accepts a fully-specified options bag", () => {
expect(() =>
_validateOptions({
url: "http://127.0.0.1:3333/",
title: "Test",
width: 1280,
height: 800,
hint: "fixed",
debug: true,
handlers: { greet: () => "hi" },
}),
).not.toThrow();
});
});
describe("@mandujs/core/desktop — createWindow peer loading", () => {
beforeEach(() => {
_resetWebviewBunCache();
});
it("validates options before attempting to load the peer", async () => {
// Bad options should throw TypeError *before* the peer import runs, so
// this test passes even on CI without webview-bun installed.
await expect(
createWindow({ url: "" } as WindowOptions),
).rejects.toThrow(TypeError);
await expect(
createWindow({} as WindowOptions),
).rejects.toThrow(TypeError);
await expect(
createWindow({
url: "javascript:evil()",
} as WindowOptions),
).rejects.toThrow(/protocol/);
});
it("throws an actionable error when webview-bun is not installed", async () => {
// Only run this when the peer is genuinely absent — if a developer has
// `webview-bun` installed locally, the import will succeed and we'd
// open a real window. Probe via dynamic import.
//
// @ts-ignore -- optional peer, resolution may fail at typecheck time
const probe = () => import("webview-bun");
let peerInstalled = false;
try {
await probe();
peerInstalled = true;
} catch {
peerInstalled = false;
}
if (peerInstalled) {
// On Windows with the peer installed, we'd end up creating a real
// window. Skip the assertion — the actionable-error guarantee is only
// meaningful when the peer is missing.
return;
}
// Ensure the FFI fallback also can't resolve — otherwise `createWindow`
// silently flips to the FFI path. We force an unreachable library path
// so both paths deterministically fail, which is the state the
// actionable error contract guards.
const originalFFIPath = process.env.MANDU_LIBWEBVIEW_PATH;
process.env.MANDU_LIBWEBVIEW_PATH =
"/path/that/does/not/exist/libwebview.so";
try {
await expect(
createWindow({ url: "http://127.0.0.1:1" }),
).rejects.toThrow(/webview-bun/);
} finally {
if (originalFFIPath !== undefined) {
process.env.MANDU_LIBWEBVIEW_PATH = originalFFIPath;
} else {
delete process.env.MANDU_LIBWEBVIEW_PATH;
}
}
});
it("MANDU_DESKTOP_INLINE_FFI=1 routes to the FFI fallback path", async () => {
// With the env flag set, `createWindow` must import
// `./webview-fallback` instead of `webview-bun`. We don't assert on
// the exact success/failure of the FFI call (that depends on the
// machine's libwebview install state) — we only assert that the
// branch is taken. The simplest observable is: when the library
// actually loads AND the test machine has a webview backend
// (WebView2 on Windows etc.), a real window would open. To stay
// CI-safe we monkey-patch dynamic import of the fallback module to
// detect invocation.
//
// Bun's ESM cache makes this tricky — instead we verify the call
// selector by checking that `createWindow` does NOT throw the
// webview-bun "install me" error when the flag is set. That error
// is only reachable via the primary `_loadWebviewBun` path; if the
// FFI branch is skipped or the webview-bun import is tried in its
// place, the test would observe the install-me error.
const originalFlag = process.env.MANDU_DESKTOP_INLINE_FFI;
const originalFFIPath = process.env.MANDU_LIBWEBVIEW_PATH;
process.env.MANDU_DESKTOP_INLINE_FFI = "1";
// Force an unreachable libwebview so the fallback fails deterministically
// and the error surfaces the fallback's distinctive language (which
// is what we assert on).
process.env.MANDU_LIBWEBVIEW_PATH = "/nonexistent/libwebview-test-stub.so";
try {
// Resolve whatever error comes back and assert it's NOT the
// webview-bun peer-missing error. The exact failure mode of the
// FFI path varies by CI machine (libwebview missing, dlopen
// succeeds on a stub, or the FFI cstring call errors), but none
// of those produce the distinctive "bun add webview-bun" string
// from the primary path.
let caught: unknown;
try {
const handle = await createWindow({ url: "http://127.0.0.1:1" });
// Unexpected success — close to clean up, then fail.
await handle.close();
throw new Error(
"unreachable — createWindow should fail with MANDU_DESKTOP_INLINE_FFI=1 + bogus MANDU_LIBWEBVIEW_PATH",
);
} catch (err) {
caught = err;
}
const msg = caught instanceof Error ? caught.message : String(caught);
// The webview-bun install-me message always mentions "bun add webview-bun".
// The fallback path's failure never does.
expect(msg).not.toContain("bun add webview-bun");
} finally {
if (originalFlag !== undefined) {
process.env.MANDU_DESKTOP_INLINE_FFI = originalFlag;
} else {
delete process.env.MANDU_DESKTOP_INLINE_FFI;
}
if (originalFFIPath !== undefined) {
process.env.MANDU_LIBWEBVIEW_PATH = originalFFIPath;
} else {
delete process.env.MANDU_LIBWEBVIEW_PATH;
}
}
});
});