// 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: () => `${++n}` }], 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(''); } })); assert.equal(t.panel.querySelector('.alert-error').textContent, ''); }); 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); }); });