| |
| |
| |
| import { describe, it, expect, beforeEach } from "vitest"; |
|
|
| |
| 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; |
| } |
|
|
| |
| let loadCalls = 0; |
| function loadAuditPane() { loadCalls++; } |
|
|
| |
| 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); |
| |
| 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); |
| }); |
|
|
| |
| |
| 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); |
| 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(true); |
| }); |
|
|
| it("修法:清掉 mockup 的 item.onclick 後 → 多選可正常取消", () => { |
| const p = buildPane(); |
| applyMockupOnclick(p); |
| p.querySelectorAll(".facet-item").forEach((it) => { it.onclick = null; }); |
| 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); |
| const items = findGroup(p, "時間範圍").querySelectorAll(".facet-item"); |
| items[1].querySelector(".facet-item__name").dispatchEvent(new MouseEvent("click", { bubbles: true })); |
| |
| expect(items[1].classList.contains("is-checked")).toBe(true); |
| }); |
| }); |
|
|