From fcb677e48d3f6ed38b1646728a1d19322738c3e9 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 20 Oct 2026 02:27:52 -0700 Subject: [PATCH] test(workspace): model select.options as a browser collection The toolbar units' fake DOM returned select.options as an Array, so the toolbar's .some() call passed there while it threw in every browser (fixed in 12228e2). options is now an indexed, iterable, frozen HTMLOptionsCollection-like object without Array methods; against the C1 toolbar three units fail with "sort.options.some is not a function". The units also assert that an unknown sort falls back to the first option and that a still-listed category survives a category refresh. --- tests/js/workspace-toolbar.test.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/js/workspace-toolbar.test.js b/tests/js/workspace-toolbar.test.js index e9188ad..c86db7a 100644 --- a/tests/js/workspace-toolbar.test.js +++ b/tests/js/workspace-toolbar.test.js @@ -6,6 +6,12 @@ const assert = require('node:assert/strict'); const toolbarModule = import('../../web/js/workspace/toolbar.mjs'); const dialogsModule = import('../../web/js/workspace/workspace-dialogs.mjs'); +function optionsCollection(list) { + const collection = { length: list.length, item: index => list[index] ?? null, [Symbol.iterator]: () => list[Symbol.iterator]() }; + list.forEach((option, index) => { collection[index] = option; }); + return Object.freeze(collection); +} + /** A minimal DOM: enough element behavior for markup, events, focus and dialogs. */ function fakeDOM() { const previous = { document: global.document }; @@ -30,7 +36,8 @@ function fakeDOM() { get textContent() { return this._text + this.children.map(child => child.textContent).join(''); } set textContent(text) { this.children = []; this._text = String(text); } get isConnected() { return this === body || Boolean(this.parentNode?.isConnected); } - get options() { return this.children.filter(child => child.tagName === 'OPTION'); } + // Like a browser's HTMLOptionsCollection: indexed and iterable, but not an Array (no .some/.map). + get options() { return optionsCollection(this.children.filter(child => child.tagName === 'OPTION')); } append(...nodes) { for (const node of nodes) { node.parentNode?.children.splice(node.parentNode.children.indexOf(node), 1); @@ -84,7 +91,7 @@ function fakeDOM() { const byId = (dom, id) => dom.all(node => node.id === id)[0]; const tags = node => node.children.map(child => child.tagName.toLowerCase()); -const optionPairs = select => select.options.map(option => [option.value, option.textContent]); +const optionPairs = select => [...select.options].map(option => [option.value, option.textContent]); const modal = dom => byId(dom, 'modal'); const tick = () => new Promise(resolve => setTimeout(resolve, 5)); const coreModule = import('../../web/js/shared/toolkit/core.js'); @@ -163,6 +170,10 @@ test('toolbar emits concept markup and order', async () => { toolbar.setValue({ q: 'moon', category: 'Archive', sort: 'newest' }); assert.deepEqual([input.value, category.value, sort.value], ['moon', 'Archive', 'newest']); assert.deepEqual(toolbar.value(), { q: 'moon', category: 'Archive', sort: 'newest' }); + // A still-listed selection survives a category refresh. + toolbar.setCategories(['Portraits', 'Archive']); + assert.equal(category.value, 'Archive'); + assert.deepEqual(optionPairs(category), [['', 'All categories'], ['Portraits', 'Portraits'], ['Archive', 'Archive']]); // A selection that is no longer listed drops to All categories. toolbar.setCategories(['Portraits']); assert.equal(category.value, '');