141 lines
4.6 KiB
JavaScript
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); |