Files
gods-eye-view/src/ui/cockpitDisplayPortal.test.mjs
T

203 lines
5.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { CockpitDisplayPortal } from './cockpitDisplayPortal.js';
function fixture() {
const prior = Object.fromEntries(
['document', 'window', 'requestAnimationFrame', 'cancelAnimationFrame'].map(
(key) => [key, globalThis[key]],
),
);
const frames = new Map();
let nextFrame = 0;
let focusCalls = 0;
let layouts = 0;
class Node extends EventTarget {
constructor(name) {
super();
this.name = name;
this.children = [];
this.parentNode = null;
this.scrollTop = 0;
this.classList = { toggle() {}, remove() {} };
}
append(node) {
node.remove();
this.children.push(node);
node.parentNode = this;
}
remove() {
if (this.parentNode)
this.parentNode.children.splice(
this.parentNode.children.indexOf(this),
1,
);
this.parentNode = null;
}
before(node) {
node.remove();
const parent = this.parentNode;
parent.children.splice(parent.children.indexOf(this), 0, node);
node.parentNode = parent;
}
after(node) {
node.remove();
const parent = this.parentNode;
parent.children.splice(parent.children.indexOf(this) + 1, 0, node);
node.parentNode = parent;
}
contains(node) {
return (
this === node || this.children.some((child) => child.contains(node))
);
}
focus() {
focusCalls += 1;
document.activeElement = this;
}
}
const standard = new Node('standard');
standard.scrollTop = 71;
const cockpit = new Node('cockpit');
cockpit.scrollTop = 19;
const group = new Node('group');
const sibling = new Node('sibling');
const slot = new Node('slot');
standard.append(group);
standard.append(sibling);
cockpit.append(slot);
cockpit.querySelector = () => slot;
globalThis.document = {
createComment: (name) => new Node(name),
activeElement: group,
body: { classList: { contains: () => false } },
};
globalThis.window = new EventTarget();
globalThis.requestAnimationFrame = (fn) => {
const id = ++nextFrame;
frames.set(id, fn);
return id;
};
globalThis.cancelAnimationFrame = (id) => frames.delete(id);
const create = () =>
new CockpitDisplayPortal({
standardPanel: standard,
cockpitPanel: cockpit,
groups: [['hud', group]],
layout: () => {
layouts += 1;
},
});
return {
standard,
cockpit,
group,
sibling,
slot,
frames,
create,
get focusCalls() {
return focusCalls;
},
get layouts() {
return layouts;
},
flush() {
while (frames.size) {
const batch = [...frames.values()];
frames.clear();
batch.forEach((fn) => fn());
}
},
restore() {
Object.assign(globalThis, prior);
},
};
}
test('Display groups return to their exact home with independent scroll positions', () => {
const f = fixture();
try {
const owner = f.create();
owner.setActive(true);
assert.equal(f.group.parentNode, f.slot);
f.cockpit.scrollTop = 0;
f.flush();
assert.equal(f.cockpit.scrollTop, 19);
assert.equal(f.focusCalls, 1);
owner.setActive(false);
f.standard.scrollTop = 0;
f.flush();
assert.equal(f.standard.scrollTop, 71);
owner.destroy();
assert.deepEqual(f.standard.children, [f.group, f.sibling]);
assert.equal(f.frames.size, 0);
} finally {
f.restore();
}
});
test('superseded portal frames cannot restore obsolete focus or scroll', () => {
const f = fixture();
try {
const owner = f.create();
owner.setActive(true);
const obsolete = [...f.frames.values()];
owner.setActive(false);
f.cockpit.scrollTop = 93;
obsolete.forEach((callback) => callback());
assert.equal(f.focusCalls, 0);
assert.equal(f.cockpit.scrollTop, 93);
f.flush();
assert.equal(f.focusCalls, 1);
assert.equal(owner.restoreOwner, null);
owner.destroy();
} finally {
f.restore();
}
});
test('disposal restores groups and revokes mode listeners and queued focus work', () => {
const f = fixture();
try {
const owner = f.create();
window.dispatchEvent(
new CustomEvent('gev:cockpit-mode-changed', { detail: { active: true } }),
);
assert.equal(f.group.parentNode, f.slot);
const obsolete = [...f.frames.values()];
owner.destroy();
owner.destroy();
obsolete.forEach((callback) => callback());
window.dispatchEvent(
new CustomEvent('gev:cockpit-mode-changed', { detail: { active: true } }),
);
owner.setActive(true);
assert.deepEqual(f.standard.children, [f.group, f.sibling]);
assert.equal(f.frames.size, 0);
assert.equal(f.focusCalls, 0);
assert.equal(f.layouts, 1);
} finally {
f.restore();
}
});
test('a stopped portal waits for cleanup without running queued focus work', () => {
const f = fixture();
try {
const owner = f.create();
owner.setActive(true);
const queued = [...f.frames.values()];
owner.stop();
queued.forEach((callback) => callback());
owner.setActive(false);
assert.equal(f.group.parentNode, f.slot);
assert.equal(f.frames.size, 0);
assert.equal(f.focusCalls, 0);
owner.destroy();
assert.deepEqual(f.standard.children, [f.group, f.sibling]);
} finally {
f.restore();
}
});