// Проверка логики initFilterChips без браузера: собираем дерево из собранной // страницы public/ai-dev/index.html и прогоняем по нему реальный модуль. import fs from "node:fs"; const HTML = fs.readFileSync(process.argv[2] || "public/ai-dev/index.html", "utf8"); class El { constructor() { this.attrs = {}; this.children = []; this._text = ""; this.listeners = {}; } setAttribute(k, v) { this.attrs[k] = String(v); } getAttribute(k) { return this.attrs[k] ?? null; } removeAttribute(k) { delete this.attrs[k]; } hasAttribute(k) { return k in this.attrs; } appendChild(c) { this.children.push(c); return c; } *walk() { for (const c of this.children) { yield c; yield* c.walk(); } } querySelector(sel) { const cls = sel.startsWith(".") ? sel.slice(1) : null; for (const d of this.walk()) { if (cls && (d.getAttribute("class") || "").split(/\s+/).includes(cls)) return d; } return null; } querySelectorAll(sel) { const attr = sel.replace(/[[\]]/g, ""); const out = []; for (const d of this.walk()) if (d.hasAttribute(attr)) out.push(d); return out; } addEventListener(ev, fn) { this.listeners[ev] = fn; } click() { this.listeners.click?.(); } get textContent() { return this._text; } set textContent(v) { this._text = String(v); } // Модуль ставит slot.className = "..." — в реальном DOM это атрибут class. set className(v) { this.attrs.class = String(v); } get className() { return this.attrs.class || ""; } } const scopeSel = (HTML.match(/data-filter-scope="([^"]+)"/) || [, "#ai-projects"])[1]; const scopeId = scopeSel.replace(/^#/, ""); const chipsRoot = new El(); chipsRoot.setAttribute("data-filter-scope", scopeSel); const scope = new El(); scope.setAttribute("id", scopeId); const chipRe = /]*class="filter-chip"[^>]*data-filter-value="([^"]+)"[^>]*aria-pressed="([^"]*)"[^>]*>([^<]*)<\/button>/g; for (const m of HTML.matchAll(chipRe)) { const li = new El(); const b = new El(); b.setAttribute("class", "filter-chip"); b.setAttribute("data-filter-value", m[1]); b.setAttribute("aria-pressed", m[2]); b.textContent = m[3]; li.appendChild(b); chipsRoot.appendChild(li); } const itemRe = /<(\w+)([^>]*\bdata-filter-item\b[^>]*)>/g; for (const m of HTML.matchAll(itemRe)) { const li = new El(); const tagm = m[2].match(/data-filter-tag="([^"]+)"/); li.setAttribute("data-filter-item", ""); li.setAttribute("data-filter-tag", tagm ? tagm[1] : ""); scope.appendChild(li); } const mod = await import("../public/js/modules/ui/filter-chips.js"); // Подменяем document на наше дерево. globalThis.document = { querySelector: (s) => { if (s === "[data-filter-chips]") return chipsRoot; if (s === scopeSel) return scope; return null; }, createElement: () => new El(), }; mod.initFilterChips(); const items = () => scope.querySelectorAll("[data-filter-item]"); const chips = () => chipsRoot.querySelectorAll("[data-filter-value]"); const visible = () => items().filter((i) => !i.hasAttribute("hidden")).length; const pressed = () => chips().filter((c) => c.getAttribute("aria-pressed") === "true").map((c) => c.getAttribute("data-filter-value")); const shownTags = () => items().filter((i) => !i.hasAttribute("hidden")).map((i) => i.getAttribute("data-filter-tag")); console.log(`chips=${chips().length} items=${items().length}`); console.log("counts in DOM:", chips().map((c) => { const slot = c.children.find((k) => (k.getAttribute("class") || "").includes("filter-chip__count")); return `${c.getAttribute("data-filter-value")}=${slot ? slot.textContent : "MISSING"}`; }).join(" ")); console.log("initial:", visible(), pressed()); let fail = 0; const tagOf = (i) => (i.getAttribute("data-filter-tag") || "").split(/\s+/); const expect = {}; for (const c of chips()) { const v = c.getAttribute("data-filter-value"); expect[v] = v === "all" ? items().length : items().filter((i) => tagOf(i).includes(v)).length; } console.log("expected:", JSON.stringify(expect)); for (const [val, want] of Object.entries(expect)) { chips().find((c) => c.getAttribute("data-filter-value") === val).click(); const got = visible(); const ok = got === want && pressed().length === 1 && pressed()[0] === val; if (!ok) fail++; console.log(`${ok ? "OK " : "FAIL"} ${val.padEnd(8)} visible=${got} (want ${want}) pressed=${pressed()} tags=${JSON.stringify(shownTags())}`); } console.log(fail === 0 ? "\nALL PASS" : `\n${fail} FAILURES`); process.exit(fail === 0 ? 0 : 1);