mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
203 lines
5.3 KiB
JavaScript
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();
|
|
}
|
|
});
|