Files
WRNexusJS/packages/compiler/test/page-loop-locals.test.ts
T
ClintchizandClaude Opus 5 2c50a07ed2 fix(compiler): expose {#each} locals to event handlers on pages
A handler expression is emitted as text and evaluated when the event fires, so
any loop variable it names has to travel with the element. Components emitted
`data-wrn-loop-locals` for this; pages did not. The same view worked inside a
component and threw ReferenceError inside a page -- with a green build and green
tests, since nothing renders the page in a browser during a build.

The CSR runtime already resolves locals generically via
closest("[data-wrn-loop-locals]"), so only codegen needed to change.

The marker is emitted only on elements that actually bind an event, and the
encoder only when a marker was produced -- but it MUST be emitted whenever one
is, or the render throws on an undefined function instead of the handler
throwing on an undefined variable, which is strictly worse. Covered by its own
test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:05:10 +05:30

86 lines
2.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { generate, parse } from "../src/index.ts";
// A `{#each}` inside a COMPONENT emitted `data-wrn-loop-locals`, so a handler
// could reference the loop variable. The same view inside a PAGE emitted the
// handler text verbatim with no locals marker, so the loop variable was not
// defined at click time and the handler threw ReferenceError -- with a green
// build and green tests, because nothing renders the page in a browser during
// a build. The runtime resolves locals with closest("[data-wrn-loop-locals]"),
// so emitting the marker is all that is required.
const view = `
{#each packs as pack}
<button @click="buyPack(pack.code)">{pack.code}</button>
{/each}
`;
const body = `
state packs = [{ code: "small" }]
functions { client async function buyPack(c) { console.log(c) } }
view {${view}}
`;
test("a page {#each} exposes loop locals to an event handler", () => {
const code = generate(parse(`page P {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
// The item and the implicit index both have to travel, since a handler may
// reference either.
expect(code).toContain('"pack": pack');
});
test("a component {#each} still exposes loop locals to an event handler", () => {
const code = generate(parse(`component C {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
expect(code).toContain('"pack": pack');
});
test("a named loop index is exposed to a page event handler", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
functions { client async function pick(c) { console.log(c) } }
view {
{#each packs as pack, i}
<button @click="pick(i)">{pack.code}</button>
{/each}
}
}`),
);
expect(code).toContain('"i": i');
});
// Only elements that actually bind an event need the marker; adding it to every
// element in a loop body would bloat the HTML for no benefit.
test("a page loop body element with no handler gets no locals marker", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
view {
{#each packs as pack}
<span>{pack.code}</span>
{/each}
}
}`),
);
expect(code).not.toContain("data-wrn-loop-locals");
});
// Emitting the marker without defining its encoder would turn a ReferenceError
// on the loop variable into a ReferenceError on the encoder -- at render time
// rather than click time, so strictly worse.
test("a page that emits loop locals also defines the encoder", () => {
const code = generate(parse(`page P {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
expect(code).toContain("function __wrnexusEncodeLoopLocals");
});
test("a page with no loop-local markers does not define the encoder", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
view { <span>{packs.length}</span> }
}`),
);
expect(code).not.toContain("__wrnexusEncodeLoopLocals");
});