Files
site-portfolio/scripts/test-filter-chips.mjs

141 lines
4.6 KiB
JavaScript

// Проверка логики 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 =
/<button[^>]*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);