mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
ShellFeedback publishes the loading status and the road network chip, and CctvControls the camera grid chip, to the gev-status store; the scene chrome renders them. SplitFlapText plays the departure-board cascade that splitFlap.js plans, writing only the label's one text node, and rests when its owner is destroyed. setSplitFlapText and disposeSplitFlap are gone.
287 lines
11 KiB
JavaScript
287 lines
11 KiB
JavaScript
// src/ui/statusChipsView.test.mjs — the split-flap label and the scene's
|
|
// status chips, rendered from the status store over happy-dom.
|
|
import '../testSupport/registerDom.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createApp, h, nextTick, ref } from 'vue';
|
|
import { createPinia } from 'pinia';
|
|
import { FLAP_SETTLE_SLACK_MS, planSplitFlap } from '../splitFlap.js';
|
|
import { useStatusStore } from './stores/status.js';
|
|
import SceneChromeView from './views/SceneChromeView.vue';
|
|
import SplitFlapText from './views/panels/SplitFlapText.vue';
|
|
|
|
// happy-dom lays nothing out: the label counts as on screen unless a test
|
|
// says otherwise, and is wider while its cells stand in for the text.
|
|
let onScreen = true;
|
|
HTMLElement.prototype.checkVisibility = function () {
|
|
return onScreen && !this.closest('[hidden]');
|
|
};
|
|
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
const width = this.querySelector('.gev-flap-cell') ? 80 : 50;
|
|
return { width, height: 10, left: 0, top: 0, right: width, bottom: 10 };
|
|
};
|
|
|
|
// A change renders, then its cells go in once the label is seen on screen.
|
|
const rendered = async () => {
|
|
for (let flush = 0; flush < 4; flush += 1) await nextTick();
|
|
};
|
|
|
|
function mountLabel(t, initial = 'LOADING LIVE DATA') {
|
|
onScreen = true;
|
|
document.body.replaceChildren();
|
|
const text = ref(initial);
|
|
const immediate = ref(false);
|
|
const app = createApp({
|
|
render: () =>
|
|
h(SplitFlapText, {
|
|
id: 'label',
|
|
text: text.value,
|
|
immediate: immediate.value,
|
|
}),
|
|
});
|
|
app.mount(document.body.appendChild(document.createElement('div')));
|
|
t.after(() => app.unmount());
|
|
const label = document.getElementById('label');
|
|
return {
|
|
text,
|
|
immediate,
|
|
label,
|
|
node: () => label.querySelector('.gev-flap-text').firstChild,
|
|
cells: () => [...label.querySelectorAll('.gev-flap-cell')],
|
|
};
|
|
}
|
|
|
|
test('the label is one text node beside empty decorative cells', (t) => {
|
|
const { label, node, cells } = mountLabel(t);
|
|
assert.equal(label.className, 'gev-flap-host');
|
|
assert.equal(node().nodeType, Node.TEXT_NODE);
|
|
assert.equal(node().data, 'LOADING LIVE DATA');
|
|
assert.equal(label.textContent, 'LOADING LIVE DATA');
|
|
assert.equal(
|
|
label.querySelector('.gev-flap-cells').getAttribute('aria-hidden'),
|
|
'true',
|
|
);
|
|
assert.equal(cells().length, 0);
|
|
});
|
|
|
|
test('a change flaps its changed columns over the same text node, then settles', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const { text, label, node, cells } = mountLabel(t);
|
|
const permanent = node();
|
|
text.value = 'LOAD COMPLETE';
|
|
await rendered();
|
|
assert.ok(node() === permanent, 'the text node never moves');
|
|
assert.equal(permanent.data, 'LOAD COMPLETE');
|
|
assert.equal(label.textContent, 'LOAD COMPLETE', 'cells carry no text');
|
|
assert.equal(label.classList.contains('gev-flap-active'), true);
|
|
assert.equal(label.style.getPropertyValue('--gev-flap-dur'), '190ms');
|
|
const plan = planSplitFlap('LOADING LIVE DATA', 'LOAD COMPLETE');
|
|
assert.equal(cells().length, plan.cells.length);
|
|
assert.deepEqual(
|
|
cells().map((cell) => cell.dataset.flapNext),
|
|
plan.cells.map((cell) => cell.to || ' '),
|
|
);
|
|
const flapping = cells().filter((cell) =>
|
|
cell.classList.contains('is-flapping'),
|
|
);
|
|
assert.equal(flapping.length, plan.changedCount);
|
|
const first = plan.cells.find((cell) => cell.changed);
|
|
assert.equal(cells()[first.index].dataset.flapPrev, first.from);
|
|
assert.equal(
|
|
cells()[first.index].style.getPropertyValue('--gev-flap-delay'),
|
|
`${first.delayMs}ms`,
|
|
);
|
|
assert.equal(cells()[0].dataset.flapPrev, undefined, 'L stays put');
|
|
t.mock.timers.tick(plan.durationMs + FLAP_SETTLE_SLACK_MS);
|
|
await rendered();
|
|
assert.equal(cells().length, 0);
|
|
assert.equal(label.classList.contains('gev-flap-active'), false);
|
|
assert.equal(label.style.getPropertyValue('--gev-flap-dur'), '');
|
|
assert.ok(node() === permanent);
|
|
});
|
|
|
|
test('an interrupted cascade rebuilds its cells so every flap restarts', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const { text, cells } = mountLabel(t, 'AAAA');
|
|
text.value = 'BBBB';
|
|
await rendered();
|
|
const before = cells();
|
|
text.value = 'CCCC';
|
|
await rendered();
|
|
assert.equal(cells().length, 4);
|
|
assert.ok(cells().every((cell) => !before.includes(cell)));
|
|
// Nothing had turned yet, so the ORIGINAL label flaps away (invariant 3).
|
|
assert.deepEqual(
|
|
cells().map((cell) => cell.dataset.flapPrev),
|
|
['A', 'A', 'A', 'A'],
|
|
);
|
|
});
|
|
|
|
test('a completion, a hidden label or an unchanged label swaps without flaps', async (t) => {
|
|
const { text, immediate, label, cells } = mountLabel(t);
|
|
immediate.value = true;
|
|
text.value = 'LOAD COMPLETE';
|
|
await rendered();
|
|
assert.equal(label.textContent, 'LOAD COMPLETE');
|
|
assert.equal(cells().length, 0);
|
|
immediate.value = false;
|
|
onScreen = false;
|
|
text.value = 'LOAD FAILED';
|
|
await rendered();
|
|
assert.equal(label.textContent, 'LOAD FAILED');
|
|
assert.equal(cells().length, 0);
|
|
assert.equal(label.classList.contains('gev-flap-active'), false);
|
|
});
|
|
|
|
test('a growing label eases to its cascade width and ends on transitionend', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const { text, label } = mountLabel(t, 'OFF');
|
|
text.value = 'LOADING';
|
|
// The ease renders its start width, lays it out, then renders the end.
|
|
for (let flush = 0; flush < 2; flush += 1) await rendered();
|
|
assert.equal(label.classList.contains('gev-flap-sizing'), true);
|
|
assert.equal(label.style.width, '80px');
|
|
const other = new Event('transitionend');
|
|
other.propertyName = 'opacity';
|
|
label.dispatchEvent(other);
|
|
await rendered();
|
|
assert.equal(label.style.width, '80px', 'only the width ease ends it');
|
|
const end = new Event('transitionend');
|
|
end.propertyName = 'width';
|
|
label.dispatchEvent(end);
|
|
await rendered();
|
|
assert.equal(label.classList.contains('gev-flap-sizing'), false);
|
|
assert.equal(label.style.width, '');
|
|
});
|
|
|
|
test('a stopped label rests at once and changes without flaps from then on', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const stopped = ref(false);
|
|
const text = ref('AAAA');
|
|
document.body.replaceChildren();
|
|
onScreen = true;
|
|
const app = createApp({
|
|
render: () =>
|
|
h(SplitFlapText, {
|
|
id: 'label',
|
|
text: text.value,
|
|
stopped: stopped.value,
|
|
}),
|
|
});
|
|
app.mount(document.body.appendChild(document.createElement('div')));
|
|
t.after(() => app.unmount());
|
|
const label = document.getElementById('label');
|
|
text.value = 'BBBB';
|
|
await rendered();
|
|
await rendered();
|
|
assert.equal(label.querySelectorAll('.gev-flap-cell').length, 4);
|
|
stopped.value = true;
|
|
await rendered();
|
|
assert.equal(label.querySelectorAll('.gev-flap-cell').length, 0);
|
|
assert.equal(label.classList.contains('gev-flap-active'), false);
|
|
text.value = 'CCCC';
|
|
await rendered();
|
|
await rendered();
|
|
assert.equal(label.textContent, 'CCCC');
|
|
assert.equal(label.querySelectorAll('.gev-flap-cell').length, 0);
|
|
});
|
|
|
|
function mountChrome(t) {
|
|
onScreen = false;
|
|
document.body.replaceChildren();
|
|
const pinia = createPinia();
|
|
const app = createApp(SceneChromeView).use(pinia);
|
|
app.mount(document.body.appendChild(document.createElement('div')));
|
|
t.after(() => app.unmount());
|
|
return { store: useStatusStore(pinia) };
|
|
}
|
|
|
|
const $ = (selector) => document.querySelector(selector);
|
|
|
|
test('before anything publishes, the chips hold their resting labels', (t) => {
|
|
mountChrome(t);
|
|
assert.equal($('#global-loading-status').hidden, true);
|
|
assert.equal($('#global-loading-status').hasAttribute('data-state'), false);
|
|
assert.equal($('#global-loading-label').textContent, 'LOADING LIVE DATA');
|
|
assert.equal($('#global-loading-detail').textContent, '');
|
|
assert.equal($('#traffic-sync-chip').classList.contains('visible'), false);
|
|
assert.equal($('#traffic-sync-label').textContent, 'syncing road network');
|
|
assert.equal($('#traffic-sync-progress').textContent, '0%');
|
|
assert.equal($('#cctv-sync-chip').classList.contains('visible'), false);
|
|
assert.equal($('#cctv-sync-label').textContent, 'loading frames');
|
|
assert.equal($('#cctv-sync-progress').textContent, '0/0');
|
|
});
|
|
|
|
test('the chips render what the status store publishes', async (t) => {
|
|
const { store } = mountChrome(t);
|
|
store.publishLoading({
|
|
hidden: false,
|
|
state: 'complete',
|
|
label: 'LOAD COMPLETE',
|
|
detail: 'Flights · Ships',
|
|
});
|
|
store.publishTraffic({
|
|
visible: true,
|
|
label: 'road network ready',
|
|
immediate: true,
|
|
progress: '',
|
|
});
|
|
store.publishCctv({
|
|
visible: true,
|
|
label: 'camera grid ready',
|
|
immediate: false,
|
|
progress: '12/12',
|
|
});
|
|
await rendered();
|
|
assert.equal($('#global-loading-status').hidden, false);
|
|
assert.equal($('#global-loading-status').dataset.state, 'complete');
|
|
assert.equal($('#global-loading-label').textContent, 'LOAD COMPLETE');
|
|
assert.equal($('#global-loading-detail').textContent, 'Flights · Ships');
|
|
assert.equal($('#traffic-sync-chip').classList.contains('visible'), true);
|
|
assert.equal($('#traffic-sync-label').textContent, 'road network ready');
|
|
assert.equal($('#traffic-sync-progress').textContent, '');
|
|
assert.equal($('#cctv-sync-chip').classList.contains('visible'), true);
|
|
assert.equal($('#cctv-sync-progress').textContent, '12/12');
|
|
$('#traffic-sync-chip').classList.add('panel-avoid');
|
|
store.publishTraffic({
|
|
visible: false,
|
|
label: 'road network ready',
|
|
immediate: true,
|
|
progress: '',
|
|
});
|
|
store.publishLoading({
|
|
hidden: true,
|
|
state: null,
|
|
label: 'LOAD COMPLETE',
|
|
detail: 'Flights · Ships',
|
|
});
|
|
await rendered();
|
|
assert.equal($('#traffic-sync-chip').classList.contains('visible'), false);
|
|
assert.equal(
|
|
$('#traffic-sync-chip').classList.contains('panel-avoid'),
|
|
true,
|
|
'classes other code sets survive',
|
|
);
|
|
assert.equal($('#global-loading-status').hidden, true);
|
|
assert.equal($('#global-loading-status').hasAttribute('data-state'), false);
|
|
assert.equal($('#global-loading-label').textContent, 'LOAD COMPLETE');
|
|
});
|
|
|
|
test('a label that changes as its status shows flaps once the status is visible', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const { store } = mountChrome(t);
|
|
onScreen = true;
|
|
store.publishLoading({
|
|
hidden: false,
|
|
state: 'error',
|
|
label: 'LOAD FAILED',
|
|
detail: '',
|
|
});
|
|
await rendered();
|
|
await rendered();
|
|
const label = $('#global-loading-label');
|
|
assert.equal($('#global-loading-status').hidden, false);
|
|
assert.equal(label.classList.contains('gev-flap-active'), true);
|
|
assert.ok(label.querySelectorAll('.gev-flap-cell.is-flapping').length > 0);
|
|
});
|