kb-demo / static /lib /audit_facet.test.js
RayLi-Git
fix: 檔案移至根目錄 + 套用示範 README
a7cd101
Raw
History Blame Contribute Delete
7.12 kB
// @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 = `
<div class="audit-layout"><aside class="facet-panel">
<div class="facet-group">
<div class="facet-group__title">時間範圍</div>
<div class="facet-item is-checked"><div class="facet-checkbox"></div><span class="facet-item__name">近 7 天</span><span class="facet-item__count">3.2k</span></div>
<div class="facet-item"><div class="facet-checkbox"></div><span class="facet-item__name">近 30 天</span><span class="facet-item__count">12.8k</span></div>
</div>
<div class="facet-group">
<div class="facet-group__title">事件類型</div>
<div class="facet-item is-checked"><div class="facet-checkbox"></div><span class="facet-item__name">🔐 驗證 / 登入</span><span class="facet-item__count">412</span></div>
<div class="facet-item is-checked"><div class="facet-checkbox"></div><span class="facet-item__name">👁 讀取</span><span class="facet-item__count">2.1k</span></div>
</div>
<div class="facet-group">
<div class="facet-group__title">風險等級</div>
<div class="facet-item is-checked"><div class="facet-checkbox"></div><span class="facet-item__name">🔴 高</span><span class="facet-item__count">3</span></div>
</div>
<div class="facet-group">
<div class="facet-group__title">使用者</div>
<div class="facet-item"><div class="facet-checkbox"></div><span class="facet-item__name">陳怡君(我)</span><span class="facet-item__count">82</span></div>
</div>
</aside></div>`;
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);
});
});