327 lines
11 KiB
TypeScript
327 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../app/i18n.js", () => ({
|
|
t: (_key, fallback) => fallback,
|
|
}));
|
|
|
|
vi.mock("../features/panel/messages/messageCenter.js", () => ({
|
|
PANEL_MESSAGES_EVENT: "mjr:test:messages",
|
|
ensurePanelMessagesReady: vi.fn(),
|
|
getPanelLastReadAt: vi.fn(() => 0),
|
|
getPanelUnreadCount: vi.fn(() => 0),
|
|
listPanelMessages: vi.fn(() => []),
|
|
markPanelMessagesRead: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../features/panel/messages/shortcutGuide.js", () => ({
|
|
createShortcutGuidePanel: vi.fn(() => ({ nodeType: "shortcut-guide" })),
|
|
}));
|
|
|
|
function createClassList() {
|
|
const names = new Set();
|
|
return {
|
|
toggle(name, active) {
|
|
if (active) names.add(name);
|
|
else names.delete(name);
|
|
},
|
|
contains(name) {
|
|
return names.has(name);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createNode() {
|
|
return {
|
|
className: "",
|
|
id: "",
|
|
hidden: false,
|
|
style: {},
|
|
textContent: "",
|
|
classList: createClassList(),
|
|
attributes: new Map(),
|
|
childNodes: [],
|
|
setAttribute(name, value) {
|
|
this.attributes.set(name, String(value));
|
|
},
|
|
replaceChildren(...children) {
|
|
this.childNodes = children;
|
|
},
|
|
appendChild(child) {
|
|
this.childNodes.push(child);
|
|
return child;
|
|
},
|
|
querySelector(selector) {
|
|
if (!String(selector || "").startsWith(".")) return null;
|
|
const cls = selector.slice(1);
|
|
const stack = [...this.childNodes];
|
|
while (stack.length) {
|
|
const node = stack.shift();
|
|
if (
|
|
String(node?.className || "")
|
|
.split(/\s+/)
|
|
.includes(cls)
|
|
)
|
|
return node;
|
|
if (Array.isArray(node?.childNodes)) stack.push(...node.childNodes);
|
|
}
|
|
return null;
|
|
},
|
|
addEventListener() {},
|
|
};
|
|
}
|
|
|
|
describe("messagePopoverController", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
const eventTarget = new EventTarget();
|
|
eventTarget.location = { href: "http://127.0.0.1:8188/" };
|
|
globalThis.window = eventTarget;
|
|
globalThis.localStorage = {
|
|
_data: new Map(),
|
|
getItem(key) {
|
|
return this._data.has(key) ? this._data.get(key) : null;
|
|
},
|
|
setItem(key, value) {
|
|
this._data.set(key, String(value));
|
|
},
|
|
clear() {
|
|
this._data.clear();
|
|
},
|
|
};
|
|
globalThis.CustomEvent = class CustomEvent extends Event {
|
|
constructor(type, init = {}) {
|
|
super(type);
|
|
this.detail = init.detail;
|
|
}
|
|
};
|
|
globalThis.document = {
|
|
createElement: vi.fn(() => createNode()),
|
|
};
|
|
});
|
|
|
|
it("shows only the shortcut panel in shortcut guide tab", async () => {
|
|
const { bindMessagePopoverController } =
|
|
await import("../features/panel/messages/messagePopoverController.js");
|
|
|
|
const badge = { style: {}, textContent: "" };
|
|
const messageBtn = {
|
|
classList: createClassList(),
|
|
setAttribute() {},
|
|
querySelector(selector) {
|
|
return selector === ".mjr-message-badge" ? badge : null;
|
|
},
|
|
addEventListener() {},
|
|
};
|
|
const title = createNode();
|
|
const messagePopover = createNode();
|
|
const messageList = createNode();
|
|
const shortcutsPanel = createNode();
|
|
const messageTabBtn = createNode();
|
|
const shortcutsTabBtn = createNode();
|
|
const markReadBtn = createNode();
|
|
const popovers = {
|
|
toggle() {},
|
|
close() {},
|
|
};
|
|
|
|
const controller = bindMessagePopoverController({
|
|
messageBtn,
|
|
messagePopover,
|
|
title,
|
|
messageList,
|
|
shortcutsPanel,
|
|
messageTabBtn,
|
|
shortcutsTabBtn,
|
|
markReadBtn,
|
|
popovers,
|
|
});
|
|
|
|
controller.showShortcutsTab();
|
|
|
|
expect(title.textContent).toBe("Shortcut Guide");
|
|
expect(messageList.hidden).toBe(true);
|
|
expect(messageList.style.display).toBe("none");
|
|
expect(messageList.attributes.get("aria-hidden")).toBe("true");
|
|
expect(shortcutsPanel.hidden).toBe(false);
|
|
expect(shortcutsPanel.style.display).toBe("");
|
|
expect(shortcutsPanel.attributes.get("aria-hidden")).toBe("false");
|
|
expect(markReadBtn.hidden).toBe(true);
|
|
|
|
controller.showMessagesTab();
|
|
|
|
expect(title.textContent).toBe("Messages");
|
|
expect(messageList.hidden).toBe(false);
|
|
expect(messageList.style.display).toBe("");
|
|
expect(shortcutsPanel.hidden).toBe(true);
|
|
expect(shortcutsPanel.style.display).toBe("none");
|
|
expect(markReadBtn.hidden).toBe(false);
|
|
});
|
|
|
|
it("renders rich toast history entries with detail and metadata", async () => {
|
|
const historyModule = await import("../features/panel/messages/toastHistory.js");
|
|
historyModule.addToastHistory({
|
|
message: "Indexed 3 assets",
|
|
title: "Vector Backfill",
|
|
detail: "Indexed 3 assets across 2 folders",
|
|
type: "success",
|
|
source: "ai-index",
|
|
status: "running",
|
|
durationMs: 3600,
|
|
progress: {
|
|
current: 3,
|
|
total: 120,
|
|
percent: 3,
|
|
eligible_total: 120,
|
|
candidate_total: 10,
|
|
indexed: 3,
|
|
skipped: 1,
|
|
errors: 0,
|
|
label: "running",
|
|
},
|
|
actionLabel: "Open docs",
|
|
actionUrl: "https://example.com/docs",
|
|
});
|
|
|
|
const { bindMessagePopoverController } =
|
|
await import("../features/panel/messages/messagePopoverController.js");
|
|
|
|
const badge = { style: {}, textContent: "" };
|
|
const messageBtn = {
|
|
classList: createClassList(),
|
|
setAttribute() {},
|
|
querySelector(selector) {
|
|
return selector === ".mjr-message-badge" ? badge : null;
|
|
},
|
|
addEventListener() {},
|
|
};
|
|
const title = createNode();
|
|
const messagePopover = createNode();
|
|
const messageList = createNode();
|
|
const historyPanel = createNode();
|
|
const messageTabBtn = createNode();
|
|
const historyTabBtn = createNode();
|
|
const shortcutsTabBtn = createNode();
|
|
const markReadBtn = createNode();
|
|
const popovers = {
|
|
toggle() {},
|
|
close() {},
|
|
};
|
|
|
|
const controller = bindMessagePopoverController({
|
|
messageBtn,
|
|
messagePopover,
|
|
title,
|
|
messageList,
|
|
historyPanel,
|
|
messageTabBtn,
|
|
historyTabBtn,
|
|
shortcutsTabBtn,
|
|
markReadBtn,
|
|
popovers,
|
|
});
|
|
|
|
controller.showHistoryTab();
|
|
|
|
expect(title.textContent).toBe("History");
|
|
const historyList = historyPanel.querySelector(".mjr-history-list");
|
|
expect(historyList).not.toBeNull();
|
|
const item = historyList.childNodes[0];
|
|
expect(item.className).toContain("mjr-history-item--success");
|
|
|
|
const content = item.childNodes[1];
|
|
expect(content.childNodes[0].textContent).toBe("Vector Backfill");
|
|
expect(content.childNodes[1].textContent).toBe("Indexed 3 assets across 2 folders");
|
|
expect(content.childNodes[2].childNodes[1].textContent).toBe("3.6s");
|
|
expect(content.childNodes[2].childNodes[2].textContent).toBe("running");
|
|
expect(content.childNodes[2].childNodes[3].textContent).toBe("ai-index");
|
|
expect(content.childNodes[3].childNodes[0].textContent).toContain("running");
|
|
expect(content.childNodes[3].childNodes[0].textContent).toContain("3/120");
|
|
expect(content.childNodes[3].childNodes[1].childNodes[0].style.width).toBe("3%");
|
|
expect(content.childNodes[4].textContent).toBe("Open docs");
|
|
});
|
|
|
|
it("auto-closes history when an automatically opened tracked job completes", async () => {
|
|
vi.useFakeTimers();
|
|
const historyModule = await import("../features/panel/messages/toastHistory.js");
|
|
historyModule.clearToastHistory();
|
|
historyModule.addToastHistory({
|
|
message: "Vector backfill running",
|
|
title: "Vector Backfill",
|
|
detail: "Running",
|
|
type: "info",
|
|
trackId: "vector-backfill:test",
|
|
operation: "vector_backfill",
|
|
status: "running",
|
|
persistent: true,
|
|
forceStore: true,
|
|
});
|
|
|
|
const { EVENTS } = await import("../app/events.js");
|
|
const { bindMessagePopoverController } =
|
|
await import("../features/panel/messages/messagePopoverController.js");
|
|
|
|
const badge = { style: {}, textContent: "" };
|
|
const messageBtn = {
|
|
classList: createClassList(),
|
|
setAttribute() {},
|
|
querySelector(selector) {
|
|
return selector === ".mjr-message-badge" ? badge : null;
|
|
},
|
|
addEventListener() {},
|
|
};
|
|
const title = createNode();
|
|
const messagePopover = createNode();
|
|
const messageList = createNode();
|
|
const historyPanel = createNode();
|
|
const messageTabBtn = createNode();
|
|
const historyTabBtn = createNode();
|
|
const shortcutsTabBtn = createNode();
|
|
const markReadBtn = createNode();
|
|
const popovers = {
|
|
toggle: vi.fn((popover) => {
|
|
popover.style.display = "block";
|
|
}),
|
|
close: vi.fn((popover) => {
|
|
popover.style.display = "none";
|
|
}),
|
|
};
|
|
|
|
bindMessagePopoverController({
|
|
messageBtn,
|
|
messagePopover,
|
|
title,
|
|
messageList,
|
|
historyPanel,
|
|
messageTabBtn,
|
|
historyTabBtn,
|
|
shortcutsTabBtn,
|
|
markReadBtn,
|
|
popovers,
|
|
});
|
|
|
|
window.dispatchEvent(new CustomEvent(EVENTS.OPEN_MESSAGE_HISTORY));
|
|
|
|
expect(messagePopover.style.display).toBe("block");
|
|
expect(title.textContent).toBe("History");
|
|
|
|
historyModule.addToastHistory({
|
|
message: "Vector backfill complete",
|
|
title: "Vector Backfill",
|
|
detail: "Done",
|
|
type: "success",
|
|
trackId: "vector-backfill:test",
|
|
operation: "vector_backfill",
|
|
status: "succeeded",
|
|
durationMs: 3000,
|
|
forceStore: true,
|
|
});
|
|
|
|
expect(messagePopover.style.display).toBe("block");
|
|
await vi.advanceTimersByTimeAsync(2200);
|
|
|
|
expect(popovers.close).toHaveBeenCalledWith(messagePopover);
|
|
expect(messagePopover.style.display).toBe("none");
|
|
vi.useRealTimers();
|
|
});
|
|
});
|