Files
WRNexusJS/packages/compiler/test/empty-constraint-attributes.test.ts
T
ClintchizandClaude Opus 5 de4477dd9c fix(compiler): never emit an empty pattern attribute
An empty pattern compiles to a regex matching only the empty string, so
every typed value becomes invalid and the form silently refuses to submit
-- no error, no request. @wrnexus/ui's input declares pattern: string = ""
and renders pattern="{pattern}", so every input that did not opt into a
pattern shipped one that could never match. This broke sign-up in a real
app, and only became visible once the dev-server client-module fix let
form enhancements mount at all.

Attributes reach the output through two emitters and both needed it: a
component's interpolated value is baked at render time, so the whole
attribute is now emitted by __wrnOptionalAttr, while a page's static
element is dropped at compile time. Component mounts are excluded, where
the value is a prop being passed down rather than an attribute.

minlength/maxlength/min/max/step/inputmode/accept get the same treatment --
inert when empty, but meaningless too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:32:32 +05:30

64 lines
2.6 KiB
TypeScript

import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { compile } from "../src/index.ts";
/**
* `pattern=""` is not inert. An empty pattern compiles to a regex that matches
* only the empty string, so EVERY typed value becomes invalid and the form
* silently refuses to submit -- no error, no request.
*
* `@wrnexus/ui`'s input declares `pattern: string = ""` and renders
* `pattern="{pattern}"`, so every input that did not opt into a pattern shipped
* one that could never match. This broke sign-up in a real app.
*
* Attributes reach the output through two different emitters, and both matter:
* a component's interpolated attribute is baked at render time via
* `__wrnAttr`, while a page's static element is serialized by `renderAttr`.
*/
const uiInput = join(import.meta.dir, "..", "..", "ui", "components", "input.wrn");
function render(view: string): string {
return compile(`page P {\n view { ${view} }\n}\n`, "P.wrn").code;
}
test("the real ui input never emits a bare pattern attribute", () => {
const code = compile(readFileSync(uiInput, "utf8"), uiInput).code;
// The broken shape: the attribute is always present, empty or not.
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
// The fixed shape: the attribute itself is decided at render time.
expect(code).toContain("__wrnOptionalAttr");
});
test("an interpolated constraint attribute on a component is emitted through the helper", () => {
const source = `component Field {\n props { pattern: string = "" }\n view { <input type="text" pattern="{pattern}" /> }\n}\n`;
const code = compile(source, "Field.wrn").code;
expect(code).toContain("__wrnOptionalAttr");
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
});
test("a static empty constraint attribute on a page element is dropped", () => {
const code = render(`<input type="email" pattern="" minlength="" />`);
expect(code).not.toContain('pattern=""');
expect(code).not.toContain('minlength=""');
});
test("a populated constraint attribute survives on both paths", () => {
expect(render(`<input type="text" pattern="[0-9]+" />`)).toContain('pattern="[0-9]+"');
const component = compile(
`component F {\n props { p: string = "x" }\n view { <input pattern="{p}" /> }\n}\n`,
"F.wrn",
).code;
expect(component).toContain("pattern");
});
test("attributes outside the constraint set keep their empty values", () => {
// An empty value or class is meaningful and must survive untouched.
const code = render(`<input type="text" value="" class="" />`);
expect(code).toContain('value=""');
});