mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 00:23:25 -06:00
e7a2d1758a
* fix(test): keep shared jsdom window in step with the per-file module reset The non-isolated runner already resets the module graph after every test file, so each file evaluates its own component classes. The jsdom window it shares across the whole worker was never reset with it: every Control UI component registers with `if (!customElements.get(tag))`, so the first file to import a component owned that tag for the rest of the run and `document.createElement` kept building elements closed over that file's module instances. Later files' singletons, module mocks, and spies were never the ones production reached, so assertions failed as "expected ... to be called once, but got 0 times" in whichever files the size-based sequencer happened to place after a warming one. Drop repo-owned tags with the graph they came from. Dependency packages are externalized and register once per worker through native ESM, so their definitions are attributed by define call site and kept. The same window also carried mounted DOM forward, so helpers reading `document.body.querySelector(...)` answered an earlier file's leaked dialog and focus assertions read its stale activeElement. Clear the body with it; `document.head` stays, since dependency styles cannot be replayed either. * fix(test): keep the jsdom definition shape local to its module
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
// Jsdom custom elements keeps shared-worker element registrations in step with the module graph.
|
|
//
|
|
// Shared (isolate: false) jsdom lanes re-evaluate the module graph for every test
|
|
// file, so each file gets freshly evaluated component classes. jsdom keeps custom
|
|
// element definitions on the window instead, outside that graph. Every Control UI
|
|
// component registers with `if (!customElements.get(tag))`, so a definition that
|
|
// survives the reset pins the tag to the previous file's class: the next file's
|
|
// `document.createElement(tag)` then builds elements closed over the earlier file's
|
|
// module instances, and its own singletons, module mocks, and spies are never the
|
|
// ones production reaches. Registry lifetime has to match graph lifetime.
|
|
//
|
|
// Only repo-owned tags may be dropped. Dependency packages are externalized, so
|
|
// they evaluate once per worker through native ESM and never re-register; dropping
|
|
// their definitions would leave `wa-*` and friends permanently unupgraded.
|
|
|
|
type JsdomCustomElementDefinition = { name: string };
|
|
|
|
export type CustomElementTracking = {
|
|
registry: CustomElementRegistry;
|
|
definitions: JsdomCustomElementDefinition[];
|
|
repoOwnedTags: Set<string>;
|
|
};
|
|
|
|
export function jsdomCustomElementDefinitions(
|
|
registry: object,
|
|
): JsdomCustomElementDefinition[] | undefined {
|
|
const implKey = Object.getOwnPropertySymbols(registry).find(
|
|
(symbol) => symbol.description === "impl",
|
|
);
|
|
if (!implKey) {
|
|
return undefined;
|
|
}
|
|
const impl = (
|
|
registry as Record<
|
|
symbol,
|
|
{ _customElementDefinitions?: JsdomCustomElementDefinition[] } | undefined
|
|
>
|
|
)[implKey];
|
|
return impl?._customElementDefinitions;
|
|
}
|
|
|
|
// Conservative on an unreadable stack: keeping a repo tag costs a stale class in
|
|
// one lane, dropping a dependency tag would leave it unupgraded for the whole run.
|
|
export function isRepoOwnedDefineStack(stack: string | undefined): boolean {
|
|
// [0] "Error", [1] the patched define in this module, [2] the module calling it.
|
|
const callerFrame = (stack ?? "").split("\n")[2] ?? "";
|
|
return callerFrame.trim() !== "" && !/[\\/]node_modules[\\/]/u.test(callerFrame);
|
|
}
|
|
|
|
// Returns undefined for anything that is not a jsdom registry: mixed lanes run
|
|
// `@vitest-environment node` files whose leftover global has no definitions to track.
|
|
export function trackCustomElementRegistry(
|
|
registry: CustomElementRegistry,
|
|
): CustomElementTracking | undefined {
|
|
const definitions = jsdomCustomElementDefinitions(registry);
|
|
if (!definitions) {
|
|
return undefined;
|
|
}
|
|
const tracking: CustomElementTracking = { registry, definitions, repoOwnedTags: new Set() };
|
|
const define = registry.define.bind(registry);
|
|
registry.define = (name, constructor, options) => {
|
|
if (isRepoOwnedDefineStack(new Error().stack)) {
|
|
tracking.repoOwnedTags.add(name);
|
|
}
|
|
define(name, constructor, options);
|
|
};
|
|
return tracking;
|
|
}
|
|
|
|
export function dropRepoOwnedCustomElements(tracking: CustomElementTracking): void {
|
|
if (tracking.repoOwnedTags.size === 0) {
|
|
return;
|
|
}
|
|
const survivors = tracking.definitions.filter(
|
|
(definition) => !tracking.repoOwnedTags.has(definition.name),
|
|
);
|
|
tracking.definitions.length = 0;
|
|
tracking.definitions.push(...survivors);
|
|
tracking.repoOwnedTags.clear();
|
|
}
|