Files
gods-eye-view/src/ui/railTestFixture.mjs
T
Sameh Khamis dcecf5c34c feat(ui): render rail cards and card blocks as components behind their builders
The rail cards, card blocks, settings, chips and row lists are Vue
components in src/ui/rail/. createRailCards() and createRailCardBlocks()
keep their update and destroy API and patch the components synchronously
with render(), so Weather and Recent imagery are unchanged. Controls of a
removed block dispatch nothing, a released range keeps its value until the
owner's next update, and range values are written only when they differ.
Weather's remount state is keyed by its panel. Tests of rendered parts run
on happy-dom.
2026-10-30 18:36:01 -07:00

141 lines
4.3 KiB
JavaScript

/** Minimal instrumented DOM for keyed rail presentation tests. */
export function railFixture(onWrite = () => {}) {
const tracked = (target) =>
new Proxy(target, {
set(object, key, value) {
onWrite(key);
return Reflect.set(object, key, value);
},
});
const document = {
createElement(tag) {
const node = new EventTarget();
Object.assign(node, {
tagName: tag.toUpperCase(),
ownerDocument: document,
children: [],
dataset: tracked({}),
style: tracked({}),
hidden: false,
textContent: '',
attributes: new Map(),
setAttribute(key, value) {
onWrite('attribute');
this.attributes.set(key, String(value));
},
getAttribute(key) {
return this.attributes.get(key) ?? null;
},
appendChild(child) {
return this.insertBefore(child, null);
},
insertBefore(child, reference) {
if (child.parent) child.remove();
onWrite('insert');
const index = reference
? this.children.indexOf(reference)
: this.children.length;
this.children.splice(index, 0, child);
child.parent = this;
return child;
},
append(...nodes) {
for (const child of nodes) this.appendChild(child);
},
remove() {
if (!this.parent) return;
onWrite('remove');
this.parent.children.splice(this.parent.children.indexOf(this), 1);
this.parent = null;
},
focus() {
document.activeElement = this;
},
matches(selector) {
if (selector.startsWith('.'))
return this.className?.split(' ').includes(selector.slice(1));
if (selector === '[data-chip-id]')
return Boolean(this.dataset.chipId);
return false;
},
closest(selector) {
return this.matches(selector) ? this : this.parent?.closest(selector);
},
click() {
if (this.disabled) return;
const target = this;
for (let node = this; node; node = node.parent) {
const event = new Event('click');
Object.defineProperty(event, 'target', { value: target });
node.dispatchEvent(event);
}
},
querySelector(selector) {
return find(
this,
(child) =>
selector.startsWith('.')
? child.className?.split(' ').includes(selector.slice(1))
: false,
false,
);
},
});
node.classList = {
contains: (name) => node.className?.split(' ').includes(name) || false,
toggle(name, enabled) {
const classes = new Set(
(node.className || '').split(' ').filter(Boolean),
);
if (enabled) classes.add(name);
else classes.delete(name);
const next = [...classes].join(' ');
if (next !== node.className) {
node.className = next;
onWrite('className');
}
},
};
return tracked(node);
},
};
const container = document.createElement('div');
const find = (root, predicate, includeRoot = true) => {
if (includeRoot && predicate(root)) return root;
for (const child of root.children) {
const result = find(child, predicate);
if (result) return result;
}
return null;
};
return {
container,
document,
find: (predicate, root = container) => find(root, predicate),
};
}
/**
* A container in the test document for presentation that renders with Vue,
* with a count of DOM writes. Import `../testSupport/registerDom.mjs` first.
*/
export function railDocumentFixture() {
const container = document.createElement('div');
document.body.appendChild(container);
const observer = new MutationObserver(() => {});
observer.observe(container, {
subtree: true,
childList: true,
attributes: true,
characterData: true,
});
return {
container,
document,
find: (predicate, root = container) =>
[root, ...root.querySelectorAll('*')].find(predicate) ?? null,
/** DOM writes since the last call. */
writes: () => observer.takeRecords().length,
};
}