complete framework remediation validation
This commit is contained in:
@@ -174,7 +174,7 @@ export function browserModuleRequired(ast: PageAst): boolean {
|
||||
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
|
||||
}
|
||||
|
||||
function functionEntry(
|
||||
function _functionEntry(
|
||||
ast: PageAst,
|
||||
fn: RuntimeFunctionDecl,
|
||||
availableFunctions: string[],
|
||||
@@ -242,7 +242,10 @@ function functionEntry(
|
||||
.join(" ");
|
||||
const peerAliases = !stateNames.length
|
||||
? functionAliases
|
||||
.map((name) => `const ${name} = (...__wrnexusPeerArgs) => context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs);`)
|
||||
.map(
|
||||
(name) =>
|
||||
`const ${name} = (...__wrnexusPeerArgs) => context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs);`,
|
||||
)
|
||||
.join("\n")
|
||||
: `const __wrnexusFlush = () => { ${syncStateToContext} };
|
||||
const __wrnexusRestore = () => { ${syncStateFromContext} };
|
||||
@@ -271,9 +274,15 @@ function functionEntry(
|
||||
const commitBinding = stateNames.length
|
||||
? `const __wrnexusCommit = () => { ${copyBack} };
|
||||
${!parameterNames.has("commit") && !declaredLocals.has("commit") && !functionAliases.includes("commit") ? "const commit = __wrnexusCommit;" : ""}
|
||||
${!parameterNames.has("setTimeout") && !declaredLocals.has("setTimeout") && !functionAliases.includes("setTimeout") ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
|
||||
${
|
||||
!parameterNames.has("setTimeout") &&
|
||||
!declaredLocals.has("setTimeout") &&
|
||||
!functionAliases.includes("setTimeout")
|
||||
? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
|
||||
try { return callback(...args); } finally { __wrnexusCommit(); }
|
||||
}, delay);` : ""}`
|
||||
}, delay);`
|
||||
: ""
|
||||
}`
|
||||
: "";
|
||||
const body = eraseFunctionTypes(fn.body);
|
||||
const runtimeBindings = [
|
||||
@@ -312,10 +321,7 @@ export function generateBrowserModule(ast: PageAst): string {
|
||||
const sharedProps = ast.props
|
||||
.map((entry) => entry.name)
|
||||
.filter(
|
||||
(name) =>
|
||||
safeIdentifier(name) &&
|
||||
!RUNTIME_BINDINGS.has(name) &&
|
||||
!sharedState.includes(name),
|
||||
(name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !sharedState.includes(name),
|
||||
);
|
||||
const callableAliases = functionNames.filter(
|
||||
(name) =>
|
||||
@@ -362,9 +368,13 @@ function __wrnexusCreateClientFunctions(context) {
|
||||
const __wrnexusCommit = () => { ${sharedCommit} };
|
||||
const __wrnexusRestore = () => { ${sharedRestore} };
|
||||
${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
|
||||
${!hasAuthoredSetTimeout ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
|
||||
${
|
||||
!hasAuthoredSetTimeout
|
||||
? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
|
||||
try { return callback(...args); } finally { __wrnexusCommit(); }
|
||||
}, delay);` : ""}
|
||||
}, delay);`
|
||||
: ""
|
||||
}
|
||||
const implementations = {
|
||||
${implementations}
|
||||
};
|
||||
|
||||
@@ -752,8 +752,7 @@ function renderNestedComponentInvocation(
|
||||
|
||||
if (
|
||||
!attr.value.includes("{") ||
|
||||
(!exprRefsState(attr.value, ctx.stateNames) &&
|
||||
!exprRefsState(attr.value, ctx.propNames))
|
||||
(!exprRefsState(attr.value, ctx.stateNames) && !exprRefsState(attr.value, ctx.propNames))
|
||||
) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
@@ -328,9 +328,7 @@ test("stateful component keeps both prop and state text reactive", async () => {
|
||||
const out = render({ start: "10", label: "Score" });
|
||||
expect(out).toContain('data-scope="start: 10, label: "Score", count: 10"');
|
||||
expect(out).toContain('data-on-click="count++"');
|
||||
expect(out).toContain(
|
||||
'<span data-text="label">Score</span>: <span data-text="count">10</span>',
|
||||
);
|
||||
expect(out).toContain('<span data-text="label">Score</span>: <span data-text="count">10</span>');
|
||||
|
||||
expect(out).toContain('data-scope="');
|
||||
|
||||
|
||||
@@ -89,7 +89,10 @@ const CONTROLLER_LOADER = String.raw`
|
||||
/** The reactive runtime served at `/__wrnexus/reactive.js` (plain browser JS). */
|
||||
export function getReactiveRuntime(development = false): string {
|
||||
let source = stripControllerSections(runtimeForMode(development));
|
||||
source = source.replace(" function hydrateScopes(root) {", `${CONTROLLER_LOADER}\n function hydrateScopes(root) {`);
|
||||
source = source.replace(
|
||||
" function hydrateScopes(root) {",
|
||||
`${CONTROLLER_LOADER}\n function hydrateScopes(root) {`,
|
||||
);
|
||||
source = source.replace(
|
||||
" hydrateNavbarControllers(host);\n hydratePreferenceControllers(host);\n hydrateSelectControllers(host);\n hydratePinInputControllers(host);",
|
||||
" loadComponentControllers(host);",
|
||||
|
||||
@@ -60,7 +60,10 @@ test("split runtime hydrates a controller only from the controller asset", () =>
|
||||
});
|
||||
|
||||
test("a page without controller markers does not request the controller asset", () => {
|
||||
const win = mount(`<main data-scope="count: 1"><span>{count}</span></main>`, getReactiveRuntime(true));
|
||||
const win = mount(
|
||||
`<main data-scope="count: 1"><span>{count}</span></main>`,
|
||||
getReactiveRuntime(true),
|
||||
);
|
||||
expect(win.document.querySelector('script[src$="controllers.js"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -703,20 +706,27 @@ test("a camelCase output reaches a parent binding despite attribute lowercasing"
|
||||
|
||||
test("development runtime warns once for an output with no parent binding", () => {
|
||||
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||
win.document.body.innerHTML =
|
||||
`<div data-scope="" data-wrn-events="complete"><button data-on-click="output.complete({})">go</button></div>`;
|
||||
win.document.body.innerHTML = `<div data-scope="" data-wrn-events="complete"><button data-on-click="output.complete({})">go</button></div>`;
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
(globalThis as Record<string, unknown>).location = win.location;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||
win as unknown as { NodeFilter: unknown }
|
||||
).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||
win as unknown as { MutationObserver: unknown }
|
||||
).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (
|
||||
win as unknown as { CustomEvent: unknown }
|
||||
).CustomEvent;
|
||||
const warnings: unknown[][] = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => warnings.push(args);
|
||||
try {
|
||||
(0, eval)(getReactiveRuntime(true));
|
||||
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(win.document);
|
||||
(
|
||||
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
|
||||
).__wrnexusHydrateScopes?.(win.document);
|
||||
(win.document.querySelector("button") as unknown as HTMLElement).click();
|
||||
(win.document.querySelector("button") as unknown as HTMLElement).click();
|
||||
} finally {
|
||||
@@ -730,15 +740,21 @@ test("development runtime warns when a component binding names a missing functio
|
||||
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||
win.document.body.innerHTML =
|
||||
`<div data-scope="">` +
|
||||
`<div data-scope="" data-wrn-hydration="Child:x">` +
|
||||
`<div data-wrn-events="save" data-wrn-out-save="missingSave(payload)"></div>` +
|
||||
`</div></div>`;
|
||||
`<div data-scope="" data-wrn-hydration="Child:x">` +
|
||||
`<div data-wrn-events="save" data-wrn-out-save="missingSave(payload)"></div>` +
|
||||
`</div></div>`;
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
(globalThis as Record<string, unknown>).location = win.location;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||
win as unknown as { NodeFilter: unknown }
|
||||
).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||
win as unknown as { MutationObserver: unknown }
|
||||
).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (
|
||||
win as unknown as { CustomEvent: unknown }
|
||||
).CustomEvent;
|
||||
const warnings: unknown[][] = [];
|
||||
const originalWarn = console.warn;
|
||||
const originalError = console.error;
|
||||
@@ -746,7 +762,9 @@ test("development runtime warns when a component binding names a missing functio
|
||||
console.error = () => {};
|
||||
try {
|
||||
(0, eval)(getReactiveRuntime(true));
|
||||
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(win.document);
|
||||
(
|
||||
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
|
||||
).__wrnexusHydrateScopes?.(win.document);
|
||||
const target = win.document.querySelector("[data-wrn-events]") as unknown as {
|
||||
__wrnexusOutputHandlers?: Record<string, Set<(payload: unknown) => unknown>>;
|
||||
};
|
||||
@@ -770,15 +788,23 @@ test("development runtime warns for referenced theme tokens absent from rendered
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
(globalThis as Record<string, unknown>).location = win.location;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent;
|
||||
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||
win as unknown as { NodeFilter: unknown }
|
||||
).NodeFilter;
|
||||
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||
win as unknown as { MutationObserver: unknown }
|
||||
).MutationObserver;
|
||||
(globalThis as Record<string, unknown>).CustomEvent = (
|
||||
win as unknown as { CustomEvent: unknown }
|
||||
).CustomEvent;
|
||||
const warnings: unknown[][] = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => warnings.push(args);
|
||||
try {
|
||||
(0, eval)(getReactiveRuntime(true));
|
||||
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(win.document);
|
||||
(
|
||||
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
|
||||
).__wrnexusHydrateScopes?.(win.document);
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ test("a component nested in another component's slot keeps its boundary", async
|
||||
return { render: (props: Record<string, string>) => `<b class="badge">${props.label}</b>` };
|
||||
}
|
||||
return {
|
||||
default: () => '<div data-component="Card"><div data-component="Badge" label="x"></div></div>',
|
||||
default: () =>
|
||||
'<div data-component="Card"><div data-component="Badge" label="x"></div></div>',
|
||||
};
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"sourceDocuments": [
|
||||
"Screenshot-directed WRNexus UI catalog reset"
|
||||
],
|
||||
"sourceDocuments": ["Screenshot-directed WRNexus UI catalog reset"],
|
||||
"components": [
|
||||
{
|
||||
"name": "Accordion",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -121,7 +121,7 @@ component DataTable {
|
||||
state query = ""
|
||||
state sortKey = ""
|
||||
state sortDirection = "asc"
|
||||
state page = 1
|
||||
state paginationPage = 1
|
||||
state perPage = pageSize
|
||||
state selectedKeys = []
|
||||
state remoteRows = []
|
||||
@@ -273,7 +273,7 @@ component DataTable {
|
||||
// page number past the end, and a table showing nothing with rows
|
||||
// available is worse than one that quietly lands on the last page.
|
||||
shared function currentPage() {
|
||||
return Math.min(Math.max(1, Number(page) || 1), pageCount())
|
||||
return Math.min(Math.max(1, Number(paginationPage) || 1), pageCount())
|
||||
}
|
||||
|
||||
shared function visibleRows() {
|
||||
@@ -576,7 +576,7 @@ component DataTable {
|
||||
sortKey = key
|
||||
sortDirection = "asc"
|
||||
}
|
||||
page = 1
|
||||
paginationPage = 1
|
||||
output.sort({ key: sortKey, direction: sortDirection })
|
||||
reload()
|
||||
output.change(viewState())
|
||||
@@ -584,7 +584,7 @@ component DataTable {
|
||||
|
||||
client function updateQuery(sourceEvent) {
|
||||
query = sourceEvent.target.value
|
||||
page = 1
|
||||
paginationPage = 1
|
||||
output.search({ query: query })
|
||||
reload()
|
||||
output.change(viewState())
|
||||
@@ -592,7 +592,7 @@ component DataTable {
|
||||
|
||||
client function goToPage(next) {
|
||||
var target = Math.min(Math.max(1, next), pageCount())
|
||||
page = target
|
||||
paginationPage = target
|
||||
output.pageChange({ page: target, pageSize: Number(perPage) || 10 })
|
||||
reload()
|
||||
output.change(viewState())
|
||||
@@ -602,7 +602,7 @@ component DataTable {
|
||||
// takes the Dropdown select detail directly.
|
||||
client function pickPageSize(detail) {
|
||||
perPage = Number(detail && detail.value) || 10
|
||||
page = 1
|
||||
paginationPage = 1
|
||||
output.pageChange({ page: 1, pageSize: perPage })
|
||||
reload()
|
||||
output.change(viewState())
|
||||
@@ -613,7 +613,7 @@ component DataTable {
|
||||
// clicks.
|
||||
client function clearSearch() {
|
||||
query = ""
|
||||
page = 1
|
||||
paginationPage = 1
|
||||
output.search({ query: "" })
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -3227,7 +3227,12 @@ test("list selects items without hrefs as well as navigation items", async () =>
|
||||
test("chart renders accessible SVG bars and emits point and legend outputs", async () => {
|
||||
const source = readFileSync(uiComponentPath("Chart"), "utf8");
|
||||
const dom = mountHtml(
|
||||
await renderComponent(source, { items: [{ label: "Alpha", value: 12 }, { label: "Beta", value: 6 }] }),
|
||||
await renderComponent(source, {
|
||||
items: [
|
||||
{ label: "Alpha", value: 12 },
|
||||
{ label: "Beta", value: 6 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
const root = dom.querySelector('[data-ui-component="Chart"]') as HTMLElement;
|
||||
const events: string[] = [];
|
||||
@@ -3251,7 +3256,8 @@ test("tree view expands branches, selects nodes, and emits public outputs", asyn
|
||||
);
|
||||
const root = dom.querySelector('[data-ui-component="TreeView"]') as HTMLElement;
|
||||
const events: string[] = [];
|
||||
for (const name of ["toggle", "expand", "select"]) root.addEventListener(name, () => events.push(name));
|
||||
for (const name of ["toggle", "expand", "select"])
|
||||
root.addEventListener(name, () => events.push(name));
|
||||
(root.querySelector(".wire-tree-view__toggle") as HTMLButtonElement).click();
|
||||
expect(root.querySelector(".wire-tree-view__group")?.getAttribute("data-show")).toBe("true");
|
||||
(root.querySelector('[aria-level="2"] .wire-tree-view__node') as HTMLButtonElement).click();
|
||||
@@ -3259,7 +3265,12 @@ test("tree view expands branches, selects nodes, and emits public outputs", asyn
|
||||
});
|
||||
|
||||
test("confetti completes deferred bursts and clipboard reports successful copies", async () => {
|
||||
const confetti = mountHtml(await renderComponent(readFileSync(uiComponentPath("Confetti"), "utf8"), { duration: 5, count: 4 }));
|
||||
const confetti = mountHtml(
|
||||
await renderComponent(readFileSync(uiComponentPath("Confetti"), "utf8"), {
|
||||
duration: 5,
|
||||
count: 4,
|
||||
}),
|
||||
);
|
||||
const confettiRoot = confetti.querySelector('[data-ui-component="Confetti"]') as HTMLElement;
|
||||
const confettiEvents: string[] = [];
|
||||
confettiRoot.addEventListener("start", () => confettiEvents.push("start"));
|
||||
@@ -3268,14 +3279,19 @@ test("confetti completes deferred bursts and clipboard reports successful copies
|
||||
await new Promise((resolve) => setTimeout(resolve, 15));
|
||||
expect(confettiEvents).toEqual(["start", "complete"]);
|
||||
|
||||
const clipboard = mountHtml(await renderComponent(readFileSync(uiComponentPath("Clipboard"), "utf8"), { value: "npm add wrnexus" }));
|
||||
const clipboard = mountHtml(
|
||||
await renderComponent(readFileSync(uiComponentPath("Clipboard"), "utf8"), {
|
||||
value: "npm add wrnexus",
|
||||
}),
|
||||
);
|
||||
Object.defineProperty((clipboard.window as { navigator: object }).navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText: () => Promise.resolve() },
|
||||
});
|
||||
const clipboardRoot = clipboard.querySelector('[data-ui-component="Clipboard"]') as HTMLElement;
|
||||
const clipboardEvents: string[] = [];
|
||||
for (const name of ["copy", "success"]) clipboardRoot.addEventListener(name, () => clipboardEvents.push(name));
|
||||
for (const name of ["copy", "success"])
|
||||
clipboardRoot.addEventListener(name, () => clipboardEvents.push(name));
|
||||
(clipboardRoot.querySelector("button") as HTMLButtonElement).click();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(clipboardEvents).toEqual(["copy", "success"]);
|
||||
|
||||
Reference in New Issue
Block a user