Files
AtreidesAssetsFree/tests/ui/tabs.test.mjs
T

200 lines
8.6 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:15b2f4e3c6894b2e23f02bc74d0bb5d5cabbc5c333e56f9b6ed448d6ac275271
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { installDom } from '../helpers/mini-dom.mjs';
const dom = installDom();
const { create, configureTabs, rowLayout } = await import('../../client/ui/tabs.js');
const items = () => [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, { id: 'c', label: 'C', when: () => false }];
const tabIds = t => t.bar.querySelectorAll('[data-tab]').map(b => b.dataset.tab);
const tick = () => new Promise(r => setImmediate(r));
describe('tabs: building and switching', () => {
it('a bar and a panel, the active tab marked, a filtered tab left out', () => {
const rendered = [];
const t = create(dom.host(), { items: items(), active: 'b', render: (id, p) => { rendered.push(id); p.textContent = id; } });
assert.deepEqual(tabIds(t), ['a', 'b']);
assert.equal(t.bar.querySelector('.is-on').dataset.tab, 'b');
assert.equal(t.bar.querySelector('[data-tab="b"]').getAttribute('aria-selected'), 'true');
assert.equal(t.panel.textContent, 'b');
assert.deepEqual(rendered, ['b']);
});
it('an unknown or filtered active tab falls back on the first', () => {
assert.equal(create(dom.host(), { items: items(), active: 'c' }).active, 'a');
assert.equal(create(dom.host(), { items: items(), active: 'zz' }).active, 'a');
});
it('a click switches, redraws the panel and reports; the same tab again does nothing', () => {
const changes = [];
let renders = 0;
const t = create(dom.host(), { items: items(), render: () => { renders++; }, onChange: id => changes.push(id) });
dom.click(t.bar.querySelector('[data-tab="b"]'));
dom.click(t.bar.querySelector('[data-tab="b"]'));
assert.deepEqual(changes, ['b']);
assert.equal(renders, 2);
t.select('zz');
assert.equal(t.active, 'b', 'an unknown id is ignored');
});
it('size lg, and the bar in another host when asked', () => {
const host = dom.host();
const side = dom.host();
const t = create(host, { items: items(), size: 'lg', barHost: side });
assert.equal(t.bar.className, 'tabset tabset--lg');
assert.equal(t.bar.parentNode, side);
assert.equal(t.panel.parentNode, host);
});
});
describe('tabs: the live dots', () => {
it('a dot function runs on every bar draw; a sync event redraws the bar and not the panel', () => {
let n = 0;
let renders = 0;
const t = create(dom.host(), {
items: [{ id: 'a', label: 'A', dot: () => `<i>${++n}</i>` }],
render: () => { renders++; }, syncOn: ['attention-changed'],
});
dom.fire(dom.document, 'attention-changed');
assert.equal(t.bar.querySelector('i').textContent, '2');
assert.equal(renders, 1);
t.refresh();
assert.equal(t.bar.querySelector('i').textContent, '3');
t.destroy();
});
it('a bar that left the document unbinds itself on the next event', () => {
const host = dom.host();
const t = create(host, { items: items(), syncOn: ['attention-changed'] });
const before = dom.document.listeners.length;
host.remove();
dom.fire(dom.document, 'attention-changed');
assert.equal(dom.document.listeners.length, before - 1);
assert.equal(t.bar.isConnected, false);
});
it('a second create on the same host removes the listeners of the first', () => {
const host = dom.host();
const before = dom.document.listeners.length;
create(host, { items: items(), syncOn: ['x-event'] });
create(host, { items: items(), syncOn: ['x-event'] });
assert.equal(dom.document.listeners.length, before + 1);
});
});
describe('tabs: two rows for a large bar that overflows', () => {
it('one row while the tabs fit, up to and including the last pixel', () => {
assert.deepEqual(rowLayout(11, 900, 1000), { rows: 1, cols: 11 });
assert.deepEqual(rowLayout(11, 1000, 1000), { rows: 1, cols: 11 });
});
it('two rows from one pixel over, the first row the longer one', () => {
assert.deepEqual(rowLayout(11, 1001, 1000), { rows: 2, cols: 6 });
assert.deepEqual(rowLayout(10, 1500, 1000), { rows: 2, cols: 5 });
});
it('never more than two rows, and a single tab is not split', () => {
assert.deepEqual(rowLayout(11, 5000, 1000), { rows: 2, cols: 6 });
assert.deepEqual(rowLayout(1, 1500, 1000), { rows: 1, cols: 1 });
});
class FakeResizeObserver {
constructor(cb) { this.cb = cb; this.targets = []; FakeResizeObserver.last = this; }
observe(el) { this.targets.push(el); }
disconnect() { this.targets = []; }
fire(width) { this.cb([{ contentRect: { width } }]); }
}
let layout = { reach: 0, room: 0 };
const sized = (reach, room) => { layout = { reach, room }; };
const withObserver = fn => {
const prev = globalThis.ResizeObserver;
globalThis.ResizeObserver = FakeResizeObserver;
FakeResizeObserver.last = null;
let proto = Object.getPrototypeOf(document.createElement('div'));
while (!Object.hasOwn(proto, 'getBoundingClientRect')) proto = Object.getPrototypeOf(proto);
const rect = proto.getBoundingClientRect;
proto.getBoundingClientRect = function () {
const right = this.classList.contains('tabset') ? layout.room : layout.reach;
return { left: 0, top: 0, right, bottom: 0, width: right, height: 0 };
};
try { return fn(); } finally {
globalThis.ResizeObserver = prev;
proto.getBoundingClientRect = rect;
layout = { reach: 0, room: 0 };
}
};
const eleven = () => Array.from({ length: 11 }, (_, i) => ({ id: `t${i}`, label: `T${i}` }));
it('a resize that makes the bar overflow switches it to rows with its column count, and back', () => withObserver(() => {
const t = create(dom.host(), { items: eleven(), size: 'lg' });
const ro = FakeResizeObserver.last;
assert.deepEqual(ro.targets, [t.bar]);
sized(1200, 1000);
ro.fire(1000);
assert.ok(t.bar.classList.contains('tabset--rows'));
assert.equal(t.bar.style.getPropertyValue('--tabset-cols'), '6');
sized(1200, 1600);
ro.fire(1600);
assert.equal(t.bar.classList.contains('tabset--rows'), false);
}));
it('a height change alone does not re-measure: that is the switch to two rows itself', () => withObserver(() => {
const t = create(dom.host(), { items: eleven(), size: 'lg' });
const ro = FakeResizeObserver.last;
sized(1200, 1000);
ro.fire(1000);
sized(900, 1000);
ro.fire(1000);
assert.ok(t.bar.classList.contains('tabset--rows'));
}));
it('a redraw of the bar re-measures, since a dot changes the width of a tab', () => withObserver(() => {
const t = create(dom.host(), { items: eleven(), size: 'lg' });
sized(1200, 1000);
t.refresh();
assert.ok(t.bar.classList.contains('tabset--rows'));
}));
it('a compact bar is never observed, and destroy stops observing', () => withObserver(() => {
create(dom.host(), { items: eleven() });
assert.equal(FakeResizeObserver.last, null);
const t = create(dom.host(), { items: eleven(), size: 'lg' });
t.destroy();
assert.deepEqual(FakeResizeObserver.last.targets, []);
}));
});
describe('tabs: the error net', () => {
const quiet = fn => { const e = console.error; console.error = () => {}; try { return fn(); } finally { console.error = e; } };
it('a throwing render shows the message, escaped, by default', () => {
const t = quiet(() => create(dom.host(), { items: items(), render: () => { throw new Error('<boom>'); } }));
assert.equal(t.panel.querySelector('.alert-error').textContent, '<boom>');
});
it('an async render that rejects is caught too', async () => {
const e = console.error;
console.error = () => {};
try {
const t = create(dom.host(), { items: items(), render: async () => { throw new Error('later'); } });
await tick();
assert.equal(t.panel.querySelector('.alert-error')?.textContent, 'later');
} finally { console.error = e; }
});
it('configureTabs sets how a failed panel shows, with the tab id; null restores the default', () => {
const seen = [];
configureTabs({ onError: (panel, err, id) => { seen.push([err.message, id]); panel.textContent = 'own'; } });
const t = create(dom.host(), { items: items(), active: 'b', render: () => { throw new Error('x'); } });
assert.deepEqual(seen, [['x', 'b']]);
assert.equal(t.panel.textContent, 'own');
configureTabs({ onError: null });
const u = quiet(() => create(dom.host(), { items: items(), render: () => { throw new Error('y'); } }));
assert.ok(u.panel.querySelector('.alert-error'));
});
it('without a host there is no control', () => {
assert.equal(create(null, { items: items() }), null);
});
});