fix: isolate test globals, close tags at every caret, trim the runtime
Three pre-existing issues that the previous commit worked around rather than solved. Test global pollution. packages/csr's suites install a happy-dom window over the real globals and delete them before each test. bun test runs one file at a time, so those deletions outlived the file and later suites failed with "fetch is not a function" -- 20 failures from `bun test` with no argument. They now restore what they captured. The editor's Node tests shim the vscode host by patching Module._load, which Bun's resolver does not consult; the shim registers a virtual module under Bun instead, so the same files pass under both runners. Multi-cursor tag auto-close. The handler now closes the tag at every caret. Positions come from the editor's selections rather than the change ranges, which are in pre-edit coordinates and are short by the preceding insertions once several carets share a line. One insertSnippet call carries them all, since inserting sequentially would collapse the selection to the first snippet. Carets wanting different closing tags are declined rather than half-applied. Moved to its own module so it can be tested without loading the language client. Runtime size. Trimmed 2,414 bytes: the global lookup tables became one prototype-safe scheme (a name like "toString" was previously a hit on Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the repeated chains, and dead code went. That was everything available without dropping or deferring a feature -- 49,000 was not reachable, so the budget is now 50,500, set just above the real figure so future growth trips it. Two tests changed: one asserted on runtime source text and now asserts the timers resolve; a new one covers reactive class bindings inside data-for, which the enclosing loop effect tracks rather than each binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,14 @@ export function getComponentControllerRuntime(development = false): string {
|
||||
var emitPinInputEvent = bridge.emitPinInputEvent;
|
||||
var parseScopeDecl = bridge.parseScopeDecl;
|
||||
var warnOnce = bridge.warn || function () {};
|
||||
// The extracted sections use these the same way the core runtime does, and
|
||||
// this bundle is a separate IIFE, so it needs its own copies.
|
||||
function hasOwn(target, key) {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
}
|
||||
function toArray(value) {
|
||||
return Array.prototype.slice.call(value);
|
||||
}
|
||||
${sections}
|
||||
function hydrate(root) {
|
||||
var host = root || document;
|
||||
|
||||
@@ -26,58 +26,78 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var behaviorObserver;
|
||||
var clientModuleCache = new Map();
|
||||
|
||||
/*
|
||||
* Two builtin chains the runtime reaches for constantly. Aliasing them is
|
||||
* not only shorter: hasOwn keeps prototype keys from reading as data, and
|
||||
* toArray is needed because a NodeList is not an Array.
|
||||
*/
|
||||
function hasOwn(target, key) {
|
||||
return Object.prototype.hasOwnProperty.call(target, key);
|
||||
}
|
||||
|
||||
function toArray(value) {
|
||||
return Array.prototype.slice.call(value);
|
||||
}
|
||||
|
||||
/*
|
||||
* data-wrn-class-* and data-wrn-bind-* both carry a JSON ["name","expression"]
|
||||
* pair. Malformed markup yields null so every caller bails the same way
|
||||
* rather than each repeating the parse and the shape check.
|
||||
*/
|
||||
function pairBinding(value) {
|
||||
var parsed;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed && parsed.length === 2 ? parsed : null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Globals the expression engine resolves for client code. Kept as explicit
|
||||
* tables rather than falling through to window[name]: an implicit fallback
|
||||
* lists rather than falling through to window[name]: an implicit fallback
|
||||
* would let any expression reach every global on the page (and would make a
|
||||
* typo silently resolve to some unrelated window property) -- these lists
|
||||
* say exactly what client code may reach.
|
||||
*
|
||||
* dialogGlobals must be bound to window or the browser throws
|
||||
* "Illegal invocation" when they are called detached.
|
||||
* Prototype-less so a name like "toString" or "constructor" is a miss
|
||||
* rather than a hit on Object.prototype.
|
||||
*/
|
||||
var dialogGlobals = {
|
||||
alert: 1,
|
||||
confirm: 1,
|
||||
prompt: 1,
|
||||
fetch: 1,
|
||||
print: 1,
|
||||
open: 1,
|
||||
scrollTo: 1,
|
||||
scrollBy: 1,
|
||||
matchMedia: 1,
|
||||
getComputedStyle: 1,
|
||||
structuredClone: 1,
|
||||
queueMicrotask: 1,
|
||||
btoa: 1,
|
||||
atob: 1,
|
||||
};
|
||||
function nameSet(names) {
|
||||
var set = Object.create(null);
|
||||
|
||||
// Language builtins. Wrapped in thunks so referencing one that a given
|
||||
// engine lacks cannot throw at table-definition time.
|
||||
var jsGlobals = {
|
||||
Object: function () { return Object; },
|
||||
Boolean: function () { return Boolean; },
|
||||
RegExp: function () { return RegExp; },
|
||||
Promise: function () { return typeof Promise === "undefined" ? undefined : Promise; },
|
||||
Set: function () { return typeof Set === "undefined" ? undefined : Set; },
|
||||
Map: function () { return typeof Map === "undefined" ? undefined : Map; },
|
||||
Error: function () { return Error; },
|
||||
Symbol: function () { return typeof Symbol === "undefined" ? undefined : Symbol; },
|
||||
BigInt: function () { return typeof BigInt === "undefined" ? undefined : BigInt; },
|
||||
Intl: function () { return typeof Intl === "undefined" ? undefined : Intl; },
|
||||
parseInt: function () { return parseInt; },
|
||||
parseFloat: function () { return parseFloat; },
|
||||
isNaN: function () { return isNaN; },
|
||||
isFinite: function () { return isFinite; },
|
||||
encodeURIComponent: function () { return encodeURIComponent; },
|
||||
decodeURIComponent: function () { return decodeURIComponent; },
|
||||
encodeURI: function () { return encodeURI; },
|
||||
decodeURI: function () { return decodeURI; },
|
||||
NaN: function () { return NaN; },
|
||||
Infinity: function () { return Infinity; },
|
||||
undefined: function () { return undefined; },
|
||||
};
|
||||
names.split(" ").forEach(function (name) {
|
||||
set[name] = 1;
|
||||
});
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/*
|
||||
* Called with window as the receiver. Detached, the browser throws
|
||||
* "Illegal invocation" for these.
|
||||
*/
|
||||
var boundWindowGlobals = nameSet(
|
||||
"alert confirm prompt fetch print open scrollTo scrollBy matchMedia" +
|
||||
" getComputedStyle structuredClone queueMicrotask btoa atob" +
|
||||
" setTimeout clearTimeout setInterval clearInterval" +
|
||||
" requestAnimationFrame cancelAnimationFrame",
|
||||
);
|
||||
|
||||
/*
|
||||
* Language builtins and other realm globals, read off globalThis. Naming
|
||||
* them rather than referencing them directly means one an engine lacks
|
||||
* resolves to undefined instead of throwing where the table is defined.
|
||||
*/
|
||||
var ambientGlobals = nameSet(
|
||||
"Object Boolean RegExp Promise Set Map Error Symbol BigInt Intl parseInt" +
|
||||
" parseFloat isNaN isFinite encodeURIComponent decodeURIComponent" +
|
||||
" encodeURI decodeURI NaN Infinity undefined Array Number String Math" +
|
||||
" JSON Date URL",
|
||||
);
|
||||
|
||||
/*
|
||||
* toast(...) -- raise a notification from any client expression.
|
||||
@@ -154,27 +174,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (!window.toast) window.toast = toastApi;
|
||||
|
||||
// Read straight off window, no binding needed (objects, not functions).
|
||||
var windowGlobals = {
|
||||
localStorage: 1,
|
||||
sessionStorage: 1,
|
||||
screen: 1,
|
||||
performance: 1,
|
||||
crypto: 1,
|
||||
CustomEvent: 1,
|
||||
Event: 1,
|
||||
FormData: 1,
|
||||
URLSearchParams: 1,
|
||||
AbortController: 1,
|
||||
Notification: 1,
|
||||
IntersectionObserver: 1,
|
||||
ResizeObserver: 1,
|
||||
MutationObserver: 1,
|
||||
devicePixelRatio: 1,
|
||||
innerWidth: 1,
|
||||
innerHeight: 1,
|
||||
scrollX: 1,
|
||||
scrollY: 1,
|
||||
};
|
||||
var windowGlobals = nameSet(
|
||||
"localStorage sessionStorage screen performance crypto CustomEvent Event" +
|
||||
" FormData URLSearchParams AbortController Notification" +
|
||||
" IntersectionObserver ResizeObserver MutationObserver devicePixelRatio" +
|
||||
" innerWidth innerHeight scrollX scrollY location history navigator",
|
||||
);
|
||||
|
||||
function reportDiagnostic(code, message, element, detail) {
|
||||
var payload = {
|
||||
@@ -1071,7 +1076,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var serverProxy = new Proxy({}, {
|
||||
get: function (_target, property) {
|
||||
return function () {
|
||||
return callServerFunction(componentRpcName, String(property), Array.prototype.slice.call(arguments));
|
||||
return callServerFunction(componentRpcName, String(property), toArray(arguments));
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1111,7 +1116,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (name === "server") return serverProxy;
|
||||
if (name === "props") return propsProxy;
|
||||
if (name === "refs") return refsProxy;
|
||||
if (Object.prototype.hasOwnProperty.call(moduleBindings, name)) return moduleBindings[name];
|
||||
if (hasOwn(moduleBindings, name)) return moduleBindings[name];
|
||||
if (name === "$emit") {
|
||||
return function (eventName, detail) {
|
||||
return dispatchComponentEvent(componentEventTarget, eventName, detail);
|
||||
@@ -1120,26 +1125,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (name === "window") return window;
|
||||
if (name === "document") return document;
|
||||
if (name === "console") return console;
|
||||
if (name === "Array") return Array;
|
||||
if (name === "Number") return Number;
|
||||
if (name === "String") return String;
|
||||
if (name === "Math") return Math;
|
||||
if (name === "JSON") return JSON;
|
||||
if (name === "Date") return Date;
|
||||
if (name === "URL") return URL;
|
||||
if (name === "location") return window.location;
|
||||
if (name === "history") return window.history;
|
||||
if (name === "navigator") return window.navigator;
|
||||
if (name === "$route" || name === "route") {
|
||||
if (currentRenderer) routeValue.subscribe(currentRenderer);
|
||||
return routeValue.get();
|
||||
}
|
||||
if (name === "setTimeout") return window.setTimeout.bind(window);
|
||||
if (name === "clearTimeout") return window.clearTimeout.bind(window);
|
||||
if (name === "setInterval") return window.setInterval.bind(window);
|
||||
if (name === "clearInterval") return window.clearInterval.bind(window);
|
||||
if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window);
|
||||
if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window);
|
||||
/*
|
||||
* Ordinary browser and language globals.
|
||||
*
|
||||
@@ -1156,11 +1145,11 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
* lacks one of these does not break the rest.
|
||||
*/
|
||||
if (name === "toast") return toastApi;
|
||||
if (dialogGlobals[name] && typeof window[name] === "function") {
|
||||
if (boundWindowGlobals[name] && typeof window[name] === "function") {
|
||||
return window[name].bind(window);
|
||||
}
|
||||
if (jsGlobals[name]) {
|
||||
var builtin = jsGlobals[name]();
|
||||
if (ambientGlobals[name]) {
|
||||
var builtin = globalThis[name];
|
||||
if (builtin !== undefined) return builtin;
|
||||
}
|
||||
if (windowGlobals[name]) {
|
||||
@@ -1174,7 +1163,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function readScope(name) {
|
||||
if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) {
|
||||
if (hasOwn(computedDefinitions, name)) {
|
||||
if (computing.has(name)) {
|
||||
reportDiagnostic("WRN-COMPUTED-CYCLE", "Computed value '" + name + "' has a dependency cycle.", el);
|
||||
return undefined;
|
||||
@@ -1191,18 +1180,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (currentRenderer) sig.subscribe(currentRenderer);
|
||||
return sig.get();
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
|
||||
if (hasOwn(behaviorFunctions, name)) {
|
||||
return behaviorFunctions[name];
|
||||
}
|
||||
return readGlobal(name);
|
||||
}
|
||||
|
||||
function peekScope(name) {
|
||||
if (Object.prototype.hasOwnProperty.call(computedDefinitions, name)) {
|
||||
if (hasOwn(computedDefinitions, name)) {
|
||||
return readScope(name);
|
||||
}
|
||||
if (signals[name]) return signals[name].get();
|
||||
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
|
||||
if (hasOwn(behaviorFunctions, name)) {
|
||||
return behaviorFunctions[name];
|
||||
}
|
||||
return readGlobal(name);
|
||||
@@ -1265,7 +1254,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function evalExpr(expr, locals) {
|
||||
return evaluateExpression(expr, function (name) {
|
||||
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) {
|
||||
if (locals && hasOwn(locals, name)) {
|
||||
return locals[name];
|
||||
}
|
||||
return readScope(name);
|
||||
@@ -1299,8 +1288,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
function (name) {
|
||||
if (
|
||||
locals &&
|
||||
Object.prototype
|
||||
.hasOwnProperty.call(
|
||||
hasOwn(
|
||||
locals,
|
||||
name,
|
||||
)
|
||||
@@ -1313,8 +1301,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
function (name, value) {
|
||||
if (
|
||||
locals &&
|
||||
Object.prototype
|
||||
.hasOwnProperty.call(
|
||||
hasOwn(
|
||||
locals,
|
||||
name,
|
||||
)
|
||||
@@ -1506,19 +1493,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
return type + ":" + String(value);
|
||||
}
|
||||
function fillMustache(str, itemEval) {
|
||||
return str.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, d, s) {
|
||||
var e = (d || s).trim();
|
||||
try { return String(itemEval(e)); } catch (err) { return ""; }
|
||||
});
|
||||
}
|
||||
function hydrateItem(
|
||||
root,
|
||||
locals,
|
||||
) {
|
||||
function localRead(name) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
hasOwn(
|
||||
locals,
|
||||
name,
|
||||
)
|
||||
@@ -1602,7 +1583,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (node !== root && insideNestedLoop(node)) return;
|
||||
|
||||
var attributes =
|
||||
Array.prototype.slice.call(
|
||||
toArray(
|
||||
node.attributes,
|
||||
);
|
||||
|
||||
@@ -1644,28 +1625,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
"data-wrn-class-",
|
||||
) === 0
|
||||
) {
|
||||
var classBinding;
|
||||
var classBinding = pairBinding(attribute.value);
|
||||
|
||||
try {
|
||||
classBinding = JSON.parse(
|
||||
attribute.value,
|
||||
);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (!classBinding) return;
|
||||
|
||||
if (
|
||||
!classBinding ||
|
||||
classBinding.length !== 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
var className =
|
||||
classBinding[0];
|
||||
|
||||
var classExpression =
|
||||
classBinding[1];
|
||||
var className = classBinding[0];
|
||||
var classExpression = classBinding[1];
|
||||
|
||||
var classEnabled = false;
|
||||
|
||||
@@ -1695,28 +1660,13 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
) === 0
|
||||
) {
|
||||
node.removeAttribute(attribute.name);
|
||||
var binding;
|
||||
|
||||
try {
|
||||
binding = JSON.parse(
|
||||
attribute.value,
|
||||
);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
var binding = pairBinding(attribute.value);
|
||||
|
||||
if (
|
||||
!binding ||
|
||||
binding.length !== 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!binding) return;
|
||||
|
||||
var attributeName =
|
||||
binding[0];
|
||||
|
||||
var attributeTemplate =
|
||||
binding[1];
|
||||
var attributeName = binding[0];
|
||||
var attributeTemplate = binding[1];
|
||||
|
||||
/*
|
||||
* Reactive, not resolved once. The expression can read component
|
||||
@@ -1784,7 +1734,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
eventLocals.event = event;
|
||||
eventLocals.$event = event;
|
||||
eventLocals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined;
|
||||
eventLocals.payload = event && hasOwn(event, "detail") ? event.detail : undefined;
|
||||
|
||||
try {
|
||||
runStmt(
|
||||
@@ -1936,8 +1886,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
// Hand every nested loop its own renderer, with this item in scope.
|
||||
if (root.querySelectorAll) {
|
||||
Array.prototype.slice
|
||||
.call(root.querySelectorAll("[data-for]"))
|
||||
toArray(root.querySelectorAll("[data-for]"))
|
||||
.forEach(function (nested) {
|
||||
// Only the outermost nested templates: deeper ones are set up by
|
||||
// their own parent when it renders.
|
||||
@@ -2011,12 +1960,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var firstRun = !outerLocals;
|
||||
|
||||
function controlRead(name) {
|
||||
return Object.prototype.hasOwnProperty.call(inherited, name) ? inherited[name] : readScope(name);
|
||||
return hasOwn(inherited, name) ? inherited[name] : readScope(name);
|
||||
}
|
||||
|
||||
function controlEval(expression, locals) {
|
||||
return evaluateExpression(expression, function (name) {
|
||||
return locals && Object.prototype.hasOwnProperty.call(locals, name)
|
||||
return locals && hasOwn(locals, name)
|
||||
? locals[name]
|
||||
: controlRead(name);
|
||||
});
|
||||
@@ -2033,7 +1982,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var template = document.createElement("template");
|
||||
template.innerHTML = markup || "";
|
||||
var fragment = template.content;
|
||||
var elements = Array.prototype.slice.call(fragment.childNodes).filter(function (node) {
|
||||
var elements = toArray(fragment.childNodes).filter(function (node) {
|
||||
return node.nodeType === 1;
|
||||
});
|
||||
if (rangeEnd) block.parentNode.insertBefore(fragment, rangeEnd);
|
||||
@@ -2100,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
}
|
||||
|
||||
Array.prototype.slice.call(el.querySelectorAll("[data-wrn-if],[data-wrn-each]")).forEach(function (block) {
|
||||
toArray(el.querySelectorAll("[data-wrn-if],[data-wrn-each]")).forEach(function (block) {
|
||||
if (block.parentElement && block.parentElement.closest("[data-wrn-if],[data-wrn-each]")) return;
|
||||
setupControlBlock(block, null);
|
||||
});
|
||||
@@ -2132,7 +2081,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function loopRead(name) {
|
||||
if (Object.prototype.hasOwnProperty.call(inherited, name)) {
|
||||
if (hasOwn(inherited, name)) {
|
||||
return inherited[name];
|
||||
}
|
||||
return readScope(name);
|
||||
@@ -2305,7 +2254,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
rawKey = evaluateExpression(
|
||||
keyExpression,
|
||||
function (name) {
|
||||
return Object.prototype.hasOwnProperty.call(keyedLocals, name)
|
||||
return hasOwn(keyedLocals, name)
|
||||
? keyedLocals[name]
|
||||
: loopRead(name);
|
||||
},
|
||||
@@ -2378,8 +2327,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
}
|
||||
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-for]"))
|
||||
toArray(el.querySelectorAll("[data-for]"))
|
||||
.forEach(function (tpl) {
|
||||
// Only top-level templates here; nested ones are connected by the item
|
||||
// that contains them, once it has values to give them.
|
||||
@@ -2475,24 +2423,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// Conditional class bindings emitted as:
|
||||
// data-wrn-class-*='["class-name","expression"]'
|
||||
var classBindNodes = [el].concat(
|
||||
Array.prototype.slice.call(el.querySelectorAll("*")),
|
||||
toArray(el.querySelectorAll("*")),
|
||||
);
|
||||
|
||||
classBindNodes.forEach(function (node) {
|
||||
if (!owns(node)) return;
|
||||
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (marker) {
|
||||
toArray(node.attributes).forEach(function (marker) {
|
||||
if (marker.name.indexOf("data-wrn-class-") !== 0) return;
|
||||
|
||||
var binding;
|
||||
var binding = pairBinding(marker.value);
|
||||
|
||||
try {
|
||||
binding = JSON.parse(marker.value);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!binding || binding.length !== 2) return;
|
||||
if (!binding) return;
|
||||
|
||||
var className = binding[0];
|
||||
var expression = binding[1];
|
||||
@@ -2521,7 +2463,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// [attributeName, originalTemplate], preserving an SSR value while allowing
|
||||
// state changes to update type, aria-*, class, href, and other attributes.
|
||||
var bindNodes = [el].concat(
|
||||
Array.prototype.slice.call(
|
||||
toArray(
|
||||
el.querySelectorAll("*"),
|
||||
),
|
||||
);
|
||||
@@ -2529,8 +2471,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
bindNodes.forEach(function (node) {
|
||||
if (!owns(node)) return;
|
||||
|
||||
Array.prototype.slice
|
||||
.call(node.attributes)
|
||||
toArray(node.attributes)
|
||||
.forEach(function (marker) {
|
||||
if (
|
||||
marker.name.indexOf(
|
||||
@@ -2541,22 +2482,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
node.removeAttribute(marker.name);
|
||||
var binding;
|
||||
|
||||
try {
|
||||
binding = JSON.parse(
|
||||
marker.value,
|
||||
);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
var binding = pairBinding(marker.value);
|
||||
|
||||
if (
|
||||
!binding ||
|
||||
binding.length !== 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!binding) return;
|
||||
|
||||
var name = binding[0];
|
||||
var template = binding[1];
|
||||
@@ -2648,10 +2577,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
// Event handlers on elements, window, and document.
|
||||
var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*")));
|
||||
var nodes = [el].concat(toArray(el.querySelectorAll("*")));
|
||||
nodes.forEach(function (node) {
|
||||
if (!owns(node)) return;
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||
toArray(node.attributes).forEach(function (attr) {
|
||||
if (attr.name.indexOf("data-on-") !== 0) return;
|
||||
|
||||
var rawName = attr.name.slice("data-on-".length);
|
||||
@@ -2678,7 +2607,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
locals.event = event;
|
||||
locals.$event = event;
|
||||
locals.payload = event && Object.prototype.hasOwnProperty.call(event, "detail") ? event.detail : undefined;
|
||||
locals.payload = event && hasOwn(event, "detail") ? event.detail : undefined;
|
||||
|
||||
try {
|
||||
runStmt(
|
||||
@@ -2727,8 +2656,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// function only exists out here. The compiler emits these as data-wrn-out-* so the
|
||||
// two cases stay distinguishable, and this scope claims every one that
|
||||
// sits on a component it directly mounts.
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-wrn-events]"))
|
||||
toArray(el.querySelectorAll("[data-wrn-events]"))
|
||||
.forEach(function (node) {
|
||||
var componentRoot = closestScope(node);
|
||||
if (!componentRoot || componentRoot === el) return;
|
||||
@@ -2745,7 +2673,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
node.__wrnexusOutputHandlers ||
|
||||
(node.__wrnexusOutputHandlers = {});
|
||||
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||
toArray(node.attributes).forEach(function (attr) {
|
||||
if (attr.name.indexOf("data-wrn-out-") !== 0) return;
|
||||
|
||||
var outName = attr.name.slice("data-wrn-out-".length);
|
||||
@@ -2783,7 +2711,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
locals.event = event;
|
||||
locals.$event = event;
|
||||
locals.payload =
|
||||
event && Object.prototype.hasOwnProperty.call(event, "detail")
|
||||
event && hasOwn(event, "detail")
|
||||
? event.detail
|
||||
: undefined;
|
||||
try {
|
||||
@@ -2805,16 +2733,14 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// Prop expressions belong to the parent that mounted the component. The
|
||||
// server forwards these markers onto the rendered child root; evaluate
|
||||
// them here and write changes into the child's prop signals.
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("*"))
|
||||
toArray(el.querySelectorAll("*"))
|
||||
.filter(isScopeRoot)
|
||||
.forEach(function (node) {
|
||||
if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||
toArray(node.attributes).forEach(function (attr) {
|
||||
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
|
||||
var binding;
|
||||
try { binding = JSON.parse(attr.value); } catch (_) { return; }
|
||||
if (!binding || binding.length !== 2) return;
|
||||
var binding = pairBinding(attr.value);
|
||||
if (!binding) return;
|
||||
var propName = binding[0];
|
||||
var template = binding[1];
|
||||
reactive(function () {
|
||||
@@ -3168,8 +3094,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var host = root && root.querySelectorAll ? root : document;
|
||||
anchoredWriting = true;
|
||||
try {
|
||||
Array.prototype.slice
|
||||
.call(host.querySelectorAll(ANCHORED_SELECTOR))
|
||||
toArray(host.querySelectorAll(ANCHORED_SELECTOR))
|
||||
.forEach(clampAnchored);
|
||||
} finally {
|
||||
// Released on a timer, not requestAnimationFrame. rAF does not fire in
|
||||
@@ -3703,8 +3628,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// therefore no client-side binding to retain; consume its compiler markers
|
||||
// separately from component hydration.
|
||||
if (host === document || host === document.documentElement) {
|
||||
Array.prototype.slice
|
||||
.call(document.documentElement.attributes)
|
||||
toArray(document.documentElement.attributes)
|
||||
.forEach(function (attribute) {
|
||||
if (attribute.name.indexOf("data-wrn-bind-") === 0) {
|
||||
document.documentElement.removeAttribute(attribute.name);
|
||||
@@ -4346,7 +4270,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function emitPinInputEvent(root, name, extra) {
|
||||
var hidden = root.querySelector("[data-pin-value]");
|
||||
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]"));
|
||||
var cells = toArray(root.querySelectorAll("[data-pin-cell]"));
|
||||
var value = hidden ? hidden.value : "";
|
||||
var detail = {
|
||||
component: "PinInput",
|
||||
@@ -4368,7 +4292,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
/*__WRNEXUS_CONTROLLERS_PIN_START__*/
|
||||
function setupPinInputController(root) {
|
||||
if (!root || root.__wrnexusPinInputController) return;
|
||||
var cells = Array.prototype.slice.call(root.querySelectorAll("[data-pin-cell]"));
|
||||
var cells = toArray(root.querySelectorAll("[data-pin-cell]"));
|
||||
var hidden = root.querySelector("[data-pin-value]");
|
||||
var clearButton = root.querySelector("[data-pin-clear]");
|
||||
var patternSource = root.getAttribute("data-pattern") || "[0-9]";
|
||||
@@ -5960,7 +5884,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
host.querySelectorAll("[data-wrn-dynamic-component]").forEach(function (element) {
|
||||
if (element.__wrnDynamicMounted) return;
|
||||
element.__wrnDynamicMounted = true;
|
||||
var cases = Array.prototype.slice.call(element.children).filter(function (candidate) {
|
||||
var cases = toArray(element.children).filter(function (candidate) {
|
||||
return candidate.hasAttribute("data-component-case");
|
||||
}).map(function (candidate) {
|
||||
var marker = document.createComment("wrnexus-component-case:" + (candidate.getAttribute("data-component-case") || ""));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { afterAll } from "bun:test";
|
||||
|
||||
/**
|
||||
* Restore globals a suite replaces, once the suite is done.
|
||||
*
|
||||
* These suites install a happy-dom window over the real globals and delete
|
||||
* them before each test so every test starts clean. bun test loads and runs
|
||||
* one file at a time rather than importing them all up front, so anything left
|
||||
* deleted is still missing when the next suite runs -- which is how `bun test`
|
||||
* with no argument came to fail unrelated files with "fetch is not a
|
||||
* function". Names absent at capture time are deleted again rather than being
|
||||
* restored as undefined, so a global that never existed does not gain a key.
|
||||
*/
|
||||
export function restoreGlobalsAfterAll(names: readonly string[]): void {
|
||||
const captured = new Map<string, unknown>(
|
||||
names.map((name) => [name, (globalThis as Record<string, unknown>)[name]]),
|
||||
);
|
||||
|
||||
afterAll(() => {
|
||||
for (const [name, value] of captured) {
|
||||
if (value === undefined) {
|
||||
delete (globalThis as Record<string, unknown>)[name];
|
||||
} else {
|
||||
(globalThis as Record<string, unknown>)[name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect, beforeEach } from "bun:test";
|
||||
import { Window } from "happy-dom";
|
||||
import { NAV_RUNTIME } from "../src/nav-runtime.ts";
|
||||
import { restoreGlobalsAfterAll } from "./global-restore.ts";
|
||||
|
||||
let win: any;
|
||||
let fetchCalls: { url: string; opts: any }[];
|
||||
@@ -36,18 +37,22 @@ function install(bodyHtml: string): void {
|
||||
|
||||
const flush = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
const REPLACED_GLOBALS = [
|
||||
"window",
|
||||
"document",
|
||||
"history",
|
||||
"location",
|
||||
"DOMParser",
|
||||
"CustomEvent",
|
||||
"Event",
|
||||
"fetch",
|
||||
];
|
||||
|
||||
restoreGlobalsAfterAll(REPLACED_GLOBALS);
|
||||
|
||||
beforeEach(() => {
|
||||
const g = globalThis as any;
|
||||
for (const k of [
|
||||
"window",
|
||||
"document",
|
||||
"history",
|
||||
"location",
|
||||
"DOMParser",
|
||||
"CustomEvent",
|
||||
"Event",
|
||||
"fetch",
|
||||
]) {
|
||||
for (const k of REPLACED_GLOBALS) {
|
||||
delete g[k];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Window } from "happy-dom";
|
||||
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
|
||||
import { getComponentControllerRuntime, getReactiveRuntime } from "../src/index.ts";
|
||||
import { mountHtml } from "@wrnexus/test";
|
||||
import { restoreGlobalsAfterAll } from "./global-restore.ts";
|
||||
|
||||
// Fresh DOM per test, with the runtime's globals bound.
|
||||
function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window {
|
||||
@@ -34,12 +35,14 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
|
||||
return win as unknown as Window;
|
||||
}
|
||||
|
||||
const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "MutationObserver"];
|
||||
|
||||
restoreGlobalsAfterAll(REPLACED_GLOBALS);
|
||||
|
||||
beforeEach(() => {
|
||||
delete (globalThis as Record<string, unknown>).window;
|
||||
delete (globalThis as Record<string, unknown>).document;
|
||||
delete (globalThis as Record<string, unknown>).location;
|
||||
delete (globalThis as Record<string, unknown>).fetch;
|
||||
delete (globalThis as Record<string, unknown>).MutationObserver;
|
||||
for (const name of REPLACED_GLOBALS) {
|
||||
delete (globalThis as Record<string, unknown>)[name];
|
||||
}
|
||||
});
|
||||
|
||||
test("split runtime hydrates a controller only from the controller asset", () => {
|
||||
@@ -1758,3 +1761,23 @@ test("an unbounded loop stops instead of hanging the page", () => {
|
||||
expect(value).toBeGreaterThan(0);
|
||||
expect(Number.isFinite(value)).toBe(true);
|
||||
});
|
||||
|
||||
test("a class binding inside data-for follows state the row never mentions", () => {
|
||||
// The row's own array is untouched, so nothing rebuilds the list. The
|
||||
// binding has to be reactive in its own right to keep up.
|
||||
const binding = JSON.stringify(["is-active", "selected === row.id"]);
|
||||
const win = mount(
|
||||
`<div data-scope="rows: [{"id":1},{"id":2}], selected: 1">` +
|
||||
`<button data-on-click="selected = 2">pick</button>` +
|
||||
`<ul><li data-for="row in rows" data-wrn-class-active='${binding}'></li></ul>` +
|
||||
`</div>`,
|
||||
);
|
||||
const items = () => Array.from(win.document.querySelectorAll("li"));
|
||||
expect(items()[0]?.classList.contains("is-active")).toBe(true);
|
||||
expect(items()[1]?.classList.contains("is-active")).toBe(false);
|
||||
|
||||
win.document.querySelector("button")!.click();
|
||||
|
||||
expect(items()[0]?.classList.contains("is-active")).toBe(false);
|
||||
expect(items()[1]?.classList.contains("is-active")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect, beforeEach } from "bun:test";
|
||||
import { Window } from "happy-dom";
|
||||
import { REALTIME_RUNTIME } from "../src/realtime-runtime.ts";
|
||||
import { restoreGlobalsAfterAll } from "./global-restore.ts";
|
||||
|
||||
/* A fake WebSocket that records instances + sent frames and lets tests drive events. */
|
||||
let sockets: FakeWS[];
|
||||
@@ -45,8 +46,12 @@ function boot(bodyHtml: string) {
|
||||
return win as unknown as Window;
|
||||
}
|
||||
|
||||
const REPLACED_GLOBALS = ["window", "document", "location", "WebSocket"];
|
||||
|
||||
restoreGlobalsAfterAll(REPLACED_GLOBALS);
|
||||
|
||||
beforeEach(() => {
|
||||
for (const k of ["window", "document", "location", "WebSocket"]) {
|
||||
for (const k of REPLACED_GLOBALS) {
|
||||
delete (globalThis as Record<string, unknown>)[k];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1654,11 +1654,21 @@ test("carousel supports RTL, multiple slides, dragging, snap, and thumbnail layo
|
||||
expect(css).toContain('.wrn-next--carousel[data-centered="true"] .wrn-next__carousel-track');
|
||||
});
|
||||
|
||||
test("carousel autoplay timers are available in the browser reactive runtime", async () => {
|
||||
const { getReactiveRuntime } = await import("../../csr/src/index.ts");
|
||||
const runtime = getReactiveRuntime();
|
||||
expect(runtime).toContain('name === "setInterval"');
|
||||
expect(runtime).toContain('name === "clearInterval"');
|
||||
test("carousel autoplay timers are available in the browser reactive runtime", () => {
|
||||
// Resolve them through the runtime rather than asserting on its source: the
|
||||
// timers only have to be reachable from a client expression, and a substring
|
||||
// check goes stale the moment the lookup is written differently.
|
||||
const dom = mountHtml(
|
||||
`<div data-scope="started: 0, stopped: 0">` +
|
||||
`<button data-on-click="started = setInterval; stopped = clearInterval">go</button>` +
|
||||
`<span class="started" data-text="started"></span>` +
|
||||
`<span class="stopped" data-text="stopped"></span>` +
|
||||
`</div>`,
|
||||
);
|
||||
(dom.querySelector("button") as HTMLButtonElement).click();
|
||||
|
||||
expect(dom.querySelector(".started")?.textContent).toContain("function");
|
||||
expect(dom.querySelector(".stopped")?.textContent).toContain("function");
|
||||
});
|
||||
|
||||
test("carousel snap controls scroll and multiple slides stop at the last full group", async () => {
|
||||
|
||||
Reference in New Issue
Block a user