From 498e9cd47ed1a1d921d844716aa8d9f9327c3a36 Mon Sep 17 00:00:00 2001 From: Alexander Puzynia Date: Mon, 9 Nov 2026 11:57:44 -0800 Subject: [PATCH] fix: create queue list with an id prop so setup no longer throws (#8) (#9) ComfyUI's $el splits its selector on "." only and passes the rest to document.createElement, so $el("div#queue-list.mr-queue-list") threw InvalidCharacterError during setup and the Resolver sidebar tab was never registered (#4, #8). Pass the id as a prop instead. Add a test that every $el selector under web/ starts with a plain tag name. --- tests/test_el_selectors.mjs | 49 +++++++++++++++++++++++++++ web/resolver/actions/queue_methods.js | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/test_el_selectors.mjs diff --git a/tests/test_el_selectors.mjs b/tests/test_el_selectors.mjs new file mode 100644 index 0000000..e6803cf --- /dev/null +++ b/tests/test_el_selectors.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { readdir, readFile } from 'node:fs/promises'; +import { extname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +const WEB_ROOT = fileURLToPath(new URL('../web/', import.meta.url)); + +// ComfyUI's $el only splits its first argument on "." and passes the first part to +// document.createElement, so "div#id" or "div[attr]" throws InvalidCharacterError. +// Ids and attributes must go in the props object instead. +const EL_CALL_PATTERN = /\$el\(\s*(["'`])(.*?)\1/g; +const TAG_NAME_PATTERN = /^[a-z][a-z0-9-]*$/i; + +async function collectJavaScriptFiles(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const entryPath = resolve(directory, entry.name); + if (entry.isDirectory()) { + files.push(...await collectJavaScriptFiles(entryPath)); + } else if (entry.isFile() && extname(entry.name).toLowerCase() === '.js') { + files.push(entryPath); + } + } + + return files; +} + +test('$el selectors start with a plain tag name', async () => { + const invalid = []; + let calls = 0; + + for (const file of await collectJavaScriptFiles(WEB_ROOT)) { + const source = await readFile(file, 'utf8'); + for (const match of source.matchAll(EL_CALL_PATTERN)) { + calls += 1; + const tagName = match[2].split('.')[0]; + if (!TAG_NAME_PATTERN.test(tagName)) { + const line = source.slice(0, match.index).split('\n').length; + invalid.push(`${relative(WEB_ROOT, file)}:${line}: ${match[0]}`); + } + } + } + + assert.ok(calls > 0, 'expected to find $el calls under web/'); + assert.deepEqual(invalid, [], 'pass ids and attributes as $el props, not in the selector'); +}); diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index bd134b1..930fd3a 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -125,7 +125,7 @@ export const queueMethods = { ]); // Scrollable list - this.queueList = $el("div#queue-list.mr-queue-list"); + this.queueList = $el("div.mr-queue-list", { id: "queue-list" }); this.queueList.addEventListener('click', (event) => { const button = event.target instanceof Element ? event.target.closest('.mr-queue-remove')