feat: release design system engine, studio and component contracts
This commit is contained in:
1 parent
ff1065fb27
commit
3943b946d6
693 files changed
+175546
-3894
No files matched your search
@@ -225,3 +225,225 @@ test("generated client modules stay below decoded-size and duplication ratchets"
|
||||
expect(decoded).toBeLessThanOrEqual(13_000);
|
||||
expect(decoded / compressed).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
test("async completion commits sibling state atomically before reactive readers run", async () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component AtomicCompletion {
|
||||
state { busy: boolean = true; submitted: boolean = false }
|
||||
functions { shared function visible(){return !submitted} client async function finish(){await Promise.resolve();busy=false;submitted=true} }
|
||||
view { <p>{visible()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
let batching = false,
|
||||
reads = 0;
|
||||
const values = { busy: true, submitted: false };
|
||||
const state = new Proxy(values, {
|
||||
set(target, key, value) {
|
||||
const previous = Reflect.get(target, key);
|
||||
Reflect.set(target, key, value);
|
||||
if (!batching && previous !== value) {
|
||||
reads++;
|
||||
functions.visible();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const functions = executable.bindClientScope({
|
||||
state,
|
||||
props: {},
|
||||
output: {},
|
||||
server: {},
|
||||
refs: {},
|
||||
batch(callback: () => void) {
|
||||
batching = true;
|
||||
try {
|
||||
callback();
|
||||
} finally {
|
||||
batching = false;
|
||||
}
|
||||
reads++;
|
||||
},
|
||||
});
|
||||
await functions.finish();
|
||||
expect(values.busy).toBe(false);
|
||||
expect(values.submitted).toBe(true);
|
||||
expect(reads).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("display helpers subscribe only to their transitive inputs and never write unchanged state", () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component FocusedHelpers {
|
||||
state { events: string[] = []; title: string = ""; selected: string = "" }
|
||||
props { timezone: string = "UTC"; unrelated: string = "" }
|
||||
functions {
|
||||
shared function zone(){return timezone}
|
||||
shared function matches(item){return item===selected}
|
||||
shared function visible(){return events.filter(matches).map(item=>item+zone())}
|
||||
client function changeTitle(value){title=value}
|
||||
}
|
||||
view { <p>{visible()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
const reads: string[] = [],
|
||||
writes: string[] = [];
|
||||
const values = { events: ["chosen"], title: "draft", selected: "chosen" };
|
||||
const functions = executable.bindClientScope({
|
||||
state: new Proxy(values, {
|
||||
get(target, key) {
|
||||
reads.push(String(key));
|
||||
return Reflect.get(target, key);
|
||||
},
|
||||
set(target, key, value) {
|
||||
writes.push(String(key));
|
||||
return Reflect.set(target, key, value);
|
||||
},
|
||||
}),
|
||||
props: { timezone: "UTC", unrelated: "" },
|
||||
output: {},
|
||||
server: {},
|
||||
refs: {},
|
||||
});
|
||||
reads.length = 0;
|
||||
writes.length = 0;
|
||||
expect(functions.visible()).toEqual(["chosenUTC"]);
|
||||
expect([...new Set(reads)].sort()).toEqual(["events", "selected"]);
|
||||
expect(writes).toEqual([]);
|
||||
functions.changeTitle("new draft");
|
||||
expect(writes).toEqual(["title"]);
|
||||
expect(values.title).toBe("new draft");
|
||||
reads.length = 0;
|
||||
writes.length = 0;
|
||||
expect(functions.visible()).toEqual(["chosenUTC"]);
|
||||
expect(reads).not.toContain("title");
|
||||
expect(writes).toEqual([]);
|
||||
});
|
||||
|
||||
test("helper defaults, template expressions and returned callbacks retain state dependencies", () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component IndirectReads {
|
||||
state { selected: string = "A"; title: string = "draft"; irrelevant: number = 0 }
|
||||
functions { shared function value(input=selected){return input} shared function label(){return () => \`\${title}:\${value()}\`} }
|
||||
view { <p>{label()()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
const reads: string[] = [];
|
||||
const values = { selected: "A", title: "draft", irrelevant: 0 };
|
||||
const functions = executable.bindClientScope({
|
||||
state: new Proxy(values, {
|
||||
get(target, key) {
|
||||
reads.push(String(key));
|
||||
return Reflect.get(target, key);
|
||||
},
|
||||
}),
|
||||
props: {},
|
||||
output: {},
|
||||
server: {},
|
||||
refs: {},
|
||||
});
|
||||
reads.length = 0;
|
||||
expect(functions.label()()).toBe("draft:A");
|
||||
expect([...new Set(reads)].sort()).toEqual(["selected", "title"]);
|
||||
values.title = "updated";
|
||||
expect(functions.label()()).toBe("updated:A");
|
||||
});
|
||||
|
||||
test("a focused helper cannot overwrite unrelated state changed outside the bound closure", async () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component PreserveExternalState {
|
||||
state { count: number = 0; title: string = "draft" }
|
||||
functions { shared function countLabel(){return String(count)} client async function increment(){await Promise.resolve();count+=1} }
|
||||
view { <p>{countLabel()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
const state = { count: 0, title: "draft" };
|
||||
const functions = executable.bindClientScope({
|
||||
state,
|
||||
props: {},
|
||||
output: {},
|
||||
server: {},
|
||||
refs: {},
|
||||
});
|
||||
state.title = "external update";
|
||||
expect(functions.countLabel()).toBe("0");
|
||||
expect(state.title).toBe("external update");
|
||||
await functions.increment();
|
||||
expect(state.count).toBe(1);
|
||||
expect(state.title).toBe("external update");
|
||||
});
|
||||
|
||||
test("permission strings and method names do not create false helper dependencies", () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component PermissionHelpers {
|
||||
state { title: string = "draft" }
|
||||
props { roles: string[] = [] }
|
||||
functions { shared function canCreate(){return roles.includes('calendar.create')} client function create(){return title} }
|
||||
view { <p>{canCreate()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
const reads: string[] = [];
|
||||
const functions = executable.bindClientScope({
|
||||
state: new Proxy(
|
||||
{ title: "draft" },
|
||||
{
|
||||
get(target, key) {
|
||||
reads.push(String(key));
|
||||
return Reflect.get(target, key);
|
||||
},
|
||||
},
|
||||
),
|
||||
props: { roles: ["calendar.create"] },
|
||||
output: {},
|
||||
server: {},
|
||||
refs: {},
|
||||
});
|
||||
reads.length = 0;
|
||||
expect(functions.canCreate()).toBe(true);
|
||||
expect(reads).toEqual([]);
|
||||
});
|
||||
|
||||
test("async functions publish loading and navigation state before yielding", async () => {
|
||||
const targets = generateTargets(
|
||||
parse(`component ImmediateAsyncState {
|
||||
state { busy: boolean = false; selected: string = "old" }
|
||||
functions { shared function label(){return selected} client async function load(value){busy=true;selected=value;await server.wait();busy=false} }
|
||||
view { <p>{label()}</p> }
|
||||
}`),
|
||||
);
|
||||
const executable = new Function(
|
||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn {bindClientScope};`,
|
||||
)();
|
||||
let release: any;
|
||||
const gate = new Promise<void>((resolve) => (release = resolve));
|
||||
const state = { busy: false, selected: "old" };
|
||||
const functions = executable.bindClientScope({
|
||||
state,
|
||||
props: {},
|
||||
output: {},
|
||||
server: { wait: () => gate },
|
||||
refs: {},
|
||||
});
|
||||
const pending = functions.load("new");
|
||||
expect(state.busy).toBe(true);
|
||||
expect(state.selected).toBe("new");
|
||||
expect(functions.label()).toBe("new");
|
||||
expect(state.busy).toBe(true);
|
||||
release();
|
||||
await pending;
|
||||
expect(state.busy).toBe(false);
|
||||
expect(state.selected).toBe("new");
|
||||
});
|
||||
Reference in new issue
Block a user