import { expect, test } from "bun:test"; import { parse } from "@wrnexus/syntax"; import { generateTargets } from "../src/targets.ts"; function browserModule(inner: string): string { return generateTargets( parse(`page Repro { client { ${inner} } functions { client async function run(): Promise { const users = await api.searchUsers({ name: "Ajay" }) console.log(users) } } view {
} } `), ).browser; } const BLOCK = ` api searchUsers POST /api/users { request { body { name?: string age?: number } } response { return data.users } error { return [] } }`; test("emits an api member that calls the transport with the block's path and method", () => { const generated = browserModule(BLOCK); expect(generated).toContain("const api ="); expect(generated).toContain("searchUsers"); expect(generated).toContain('"/api/users"'); expect(generated).toContain('"POST"'); }); test("declared field types never reach the browser module", () => { // The artifact is written as .mjs and parsed as JavaScript. const generated = browserModule(BLOCK); expect(generated).not.toContain("name?: string"); expect(generated).not.toContain("age?: number"); }); test("the emitted module is valid JavaScript", () => { const generated = browserModule(BLOCK); expect(() => { new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, "")); }).not.toThrow(); }); test("a block without an error section still emits its response body", () => { const generated = browserModule(` api plainUsers GET /api/users { request { parameters { team: string } } response { return data.users } }`); expect(generated).toContain("plainUsers"); expect(generated).toContain("data.users"); }); test("type annotations in response/error bodies are erased before emission (B4)", () => { // Every other browser-bound body in the repo passes through eraseFunctionTypes // (see the fn.body call sites in client-codegen.ts ~line 288 and ~371, and // store-codegen.ts); response/error bodies must too, for the same reason: // eraseFunctionTypes strips function-signature annotations (params, return // type, typed catch clauses) so a locally-declared helper function inside a // response/error body no longer ships raw TypeScript into the .mjs artifact. const generated = browserModule(` api searchUsers POST /api/users { request { body { name?: string } } response { function pick(list: string[]): string[] { return list } return pick(data.users) } error { function describe(e: unknown): string { return String(e) } return describe(error) } }`); expect(generated).not.toContain("list: string[]"); expect(generated).not.toContain("): string[] {"); expect(generated).not.toContain("e: unknown"); expect(generated).not.toContain("): string {"); expect(() => { new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, "")); }).not.toThrow(); }); test("a page with state api and no client api blocks still reads that state (B5)", () => { // "api" is normally excluded from state/prop destructuring because the // emitted `const api = {...}` binding would shadow it -- but that binding // only exists when the page has client-mode api blocks. Without one, the // exclusion left `api` completely undeclared: a ReferenceError. const generated = generateTargets( parse(`page Repro { state { api = "hello" } functions { client function run(): void { console.log(api) } } view {
} } `), ).browser; expect(generated).toContain("context.state"); expect(() => { new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, "")); }).not.toThrow(); }); test("a state field named api does not collide with the emitted api object", () => { const generated = generateTargets( parse(`page Repro { state { api = "" } client { ${BLOCK} } functions { client async function run(): Promise { const users = await api.searchUsers({ name: "Ajay" }) console.log(users) } } view {
} } `), ).browser; expect(() => { new Function(generated.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, "")); }).not.toThrow(); });