148 lines
5.7 KiB
TypeScript
148 lines
5.7 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { generateTargets, parse } from "../src/index.ts";
|
|
|
|
const ast = parse(`component ConfirmDialog {
|
|
props { title: string open: boolean = false }
|
|
state { loading: boolean = false }
|
|
server state { internalId: string = "secret" }
|
|
outputs { confirm(payload: string) close() }
|
|
functions {
|
|
client async function confirm(value: string): Promise<void> {
|
|
const saved = await server.confirm(value)
|
|
output.confirm(saved)
|
|
}
|
|
server async function confirm(value: string): Promise<string> { return value }
|
|
shared function normalize(value: string): string { return value.trim() }
|
|
}
|
|
view { <button>{title}</button> }
|
|
}`);
|
|
|
|
test("emits separate browser and server function targets", () => {
|
|
const targets = generateTargets(ast);
|
|
expect(targets.browser).toContain('"confirm": async function');
|
|
expect(targets.browser).toContain('"normalize": function');
|
|
expect(targets.server).toContain("async function confirm");
|
|
expect(targets.rpc).toEqual([
|
|
expect.objectContaining({ component: "ConfirmDialog", function: "confirm" }),
|
|
]);
|
|
expect(targets.declarations).toContain("interface ConfirmDialogOutputs");
|
|
expect(targets.declarations).toContain("interface ConfirmDialogServerCalls");
|
|
});
|
|
|
|
test("component contracts retain declared union options", () => {
|
|
const targets = generateTargets(
|
|
parse(`component SizeBox {
|
|
props { size: "small" | "medium" | "large" = "medium" }
|
|
view { <div></div> }
|
|
}`),
|
|
);
|
|
expect(targets.contract.props[0]?.options).toEqual(["small", "medium", "large"]);
|
|
});
|
|
|
|
test("generates a standalone browser module for imported stores", () => {
|
|
const targets = generateTargets(
|
|
parse(`page store SearchStore {
|
|
state { query: string = "" }
|
|
client state { focused: boolean = false }
|
|
server state { secret: string = "hidden" }
|
|
computed { empty: boolean = query.length === 0 }
|
|
functions { client function setQuery(value: string): void { query = value } }
|
|
persist { storage = "session" include = ["query"] version = 1 }
|
|
}`),
|
|
);
|
|
expect(targets.browser).toContain("__wrnexusStoreRegistry");
|
|
expect(targets.browser).toContain('name: "SearchStore"');
|
|
expect(targets.browser).toContain('"query"');
|
|
expect(targets.browser).not.toContain('"secret": ("hidden")');
|
|
expect(targets.declarations).toContain("interface SearchStoreInstance");
|
|
});
|
|
|
|
test("browser codegen avoids reserved prop bindings and parameter collisions", () => {
|
|
const targets = generateTargets(
|
|
parse(`component ReservedBindings {
|
|
props { class: string = "" output: string = "" }
|
|
state { value: string = "" }
|
|
outputs { change(payload: { value: string }) }
|
|
functions {
|
|
client function update(output: string): void {
|
|
value = output
|
|
}
|
|
client function notify(): void {
|
|
output.change({ value: value })
|
|
}
|
|
}
|
|
view { <button>{class}</button> }
|
|
}`),
|
|
);
|
|
expect(targets.browser).not.toContain("const { class }");
|
|
expect(targets.browser).not.toContain("const output = context.output;\n const output");
|
|
expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
|
|
});
|
|
|
|
test("RPC manifests expose only server functions referenced through server.name", () => {
|
|
const targets = generateTargets(
|
|
parse(`component SecureActions {
|
|
functions {
|
|
client async function saveClient(): Promise<void> { await server.save("ok") }
|
|
server async function save(value: string): Promise<string> { return value }
|
|
server function internalSecret(): string { return "secret" }
|
|
}
|
|
view { <button @click='saveClient()'>Save</button> }
|
|
}`),
|
|
);
|
|
expect(targets.rpc.map((entry) => entry.function)).toEqual(["save"]);
|
|
expect(targets.server).toContain("internalSecret");
|
|
});
|
|
|
|
test("generated browser stores bind typed RPC, persistence validation, and HMR", () => {
|
|
const targets = generateTargets(
|
|
parse(`global store UserStore {
|
|
state { user: string | null = null count: number = 0 }
|
|
functions {
|
|
client async function refresh(): Promise<void> { user = await server.loadCurrentUser() }
|
|
server async function loadCurrentUser(): Promise<string | null> { return "Ajay" }
|
|
server function internalOnly(): string { return "secret" }
|
|
}
|
|
persist {
|
|
storage = "local"
|
|
include = ["count"]
|
|
version = 2
|
|
migrations { function migrate(value, fromVersion, toVersion) { return value } }
|
|
validate { function validate(value) { return value && typeof value === "object" ? value : null } }
|
|
}
|
|
}`),
|
|
);
|
|
expect(targets.browser).toContain("const server = context.server");
|
|
expect(targets.browser).toContain("currentDefinition.persist.migrate");
|
|
expect(targets.browser).toContain("currentDefinition.persist.validate");
|
|
expect(targets.browser).toContain("__wrnexusApplyStoreHotUpdate");
|
|
expect(targets.rpc.map((entry) => entry.function)).toEqual(["loadCurrentUser"]);
|
|
});
|
|
|
|
test("browser codegen binds peer client functions through the scoped function table", () => {
|
|
const targets = generateTargets(
|
|
parse(`component PeerCalls {
|
|
state { value: number = 0 }
|
|
functions {
|
|
client function increment(): void { value += 1 }
|
|
client function run(): void { increment() }
|
|
}
|
|
view { <button @click='run()'>{value}</button> }
|
|
}`),
|
|
);
|
|
expect(targets.browser).toContain("context.functions");
|
|
const executable = new Function(
|
|
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
|
|
)() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
|
|
const state = { value: 0 };
|
|
const functions = executable.bindClientScope({
|
|
state,
|
|
props: {},
|
|
output: {},
|
|
server: {},
|
|
refs: {},
|
|
});
|
|
functions.run?.();
|
|
expect(state.value).toBe(1);
|
|
});
|