// @vitest-environment happy-dom // 重現「稽核日誌 facet 多選項點了無法取消」bug。 // 用真實 facet DOM 結構 + 逐字複製的 click handler。 import { describe, it, expect, beforeEach } from "vitest"; // ── 真實 facet panel DOM(取自 知識庫視覺圖最終版.html)── function buildPane() { const pane = document.createElement("div"); pane.className = "admin-pane"; pane.dataset.tab = "audit"; pane.innerHTML = `
`; document.body.appendChild(pane); return pane; } // ── 逐字複製目前 app.js 的 handler(含 loadAuditPane 樁)── let loadCalls = 0; function loadAuditPane() { loadCalls++; } // 逐字複製 wireAuditPane 的冪等註冊(remove-before-add) function attachHandler(pane) { const _MULTI_FACET_GROUPS = ["事件類型", "風險等級", "結果"]; if (pane._kbv5FacetHandler) pane.removeEventListener("click", pane._kbv5FacetHandler); const facetHandler = (e) => { const item = e.target.closest(".facet-item"); if (!item || !pane.contains(item)) return; const nameEl = item.querySelector(".facet-item__name"); if (!nameEl || (nameEl.textContent || "").includes("無資料")) return; const group = item.closest(".facet-group"); if (!group) return; const title = (group.querySelector(".facet-group__title") || {}).textContent || ""; if (_MULTI_FACET_GROUPS.includes(title)) { item.classList.toggle("is-checked"); } else { const wasChecked = item.classList.contains("is-checked"); group.querySelectorAll(".facet-item").forEach((x) => x.classList.remove("is-checked")); if (!(title === "使用者" && wasChecked)) item.classList.add("is-checked"); } loadAuditPane(pane); }; pane.addEventListener("click", facetHandler); pane._kbv5FacetHandler = facetHandler; } function findGroup(pane, title) { return Array.from(pane.querySelectorAll(".facet-group")) .find((g) => g.querySelector(".facet-group__title").textContent === title); } describe("稽核 facet 點選", () => { let pane; beforeEach(() => { document.body.innerHTML = ""; loadCalls = 0; pane = buildPane(); attachHandler(pane); }); it("事件類型(多選)點已勾選項 → 應取消勾選", () => { const g = findGroup(pane, "事件類型"); const item = g.querySelector(".facet-item.is-checked"); // 第一個已勾的 expect(item.classList.contains("is-checked")).toBe(true); // 模擬點擊名稱 span(真實使用者多半點在文字上) item.querySelector(".facet-item__name").dispatchEvent(new Event("click", { bubbles: true })); expect(item.classList.contains("is-checked")).toBe(false); // ← 應已取消 }); it("點 checkbox 方塊也要能取消", () => { const g = findGroup(pane, "事件類型"); const item = g.querySelector(".facet-item.is-checked"); item.querySelector(".facet-checkbox").dispatchEvent(new Event("click", { bubbles: true })); expect(item.classList.contains("is-checked")).toBe(false); }); it("時間範圍(單選)點另一項 → 切換", () => { const g = findGroup(pane, "時間範圍"); const items = g.querySelectorAll(".facet-item"); items[1].querySelector(".facet-item__name").dispatchEvent(new Event("click", { bubbles: true })); expect(items[0].classList.contains("is-checked")).toBe(false); expect(items[1].classList.contains("is-checked")).toBe(true); }); // ── 回歸:真因 = mockup demo script 對每個 .facet-item 設了 onclick=toggle,與 app handler 抵消 ── // 模擬 mockup:document.querySelectorAll('.facet-item').forEach(i => i.onclick = () => i.classList.toggle('is-checked')) function applyMockupOnclick(p) { p.querySelectorAll(".facet-item").forEach((i) => { i.onclick = () => i.classList.toggle("is-checked"); }); } it("重現 BUG:mockup 的 item.onclick 與 app handler 同時觸發 → 多選互相抵消", () => { const p = buildPane(); applyMockupOnclick(p); // mockup 先設 onclick attachHandler(p); // app.js 委派 handler(未清 onclick) const item = findGroup(p, "事件類型").querySelector(".facet-item.is-checked"); item.querySelector(".facet-item__name").dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(item.classList.contains("is-checked")).toBe(true); // onclick toggle回 + app toggle掉 → 抵消(重現) }); it("修法:清掉 mockup 的 item.onclick 後 → 多選可正常取消", () => { const p = buildPane(); applyMockupOnclick(p); p.querySelectorAll(".facet-item").forEach((it) => { it.onclick = null; }); // ← wireAuditPane 的修法 attachHandler(p); const item = findGroup(p, "事件類型").querySelector(".facet-item.is-checked"); item.querySelector(".facet-item__name").dispatchEvent(new MouseEvent("click", { bubbles: true })); expect(item.classList.contains("is-checked")).toBe(false); // ✅ 正常取消 }); it("單選(時間範圍)即使有 mockup onclick 也正常(app 的 remove-all+add 覆蓋)", () => { const p = buildPane(); applyMockupOnclick(p); attachHandler(p); // 不清 onclick,驗證單選為何看似正常 const items = findGroup(p, "時間範圍").querySelectorAll(".facet-item"); items[1].querySelector(".facet-item__name").dispatchEvent(new MouseEvent("click", { bubbles: true })); // mockup toggle 近30天→checked; app remove-all 後 add 近30天 → 仍 checked(單選結果一致) expect(items[1].classList.contains("is-checked")).toBe(true); }); });