mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-31 15:57:06 +00:00
308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
confirmAction: vi.fn(),
|
|
pickOne: vi.fn(),
|
|
promptPassword: vi.fn(),
|
|
promptText: vi.fn(),
|
|
legacyAskSelectString: vi.fn(),
|
|
legacyAskString: vi.fn(),
|
|
legacyAskYesNo: vi.fn(),
|
|
legacyConfirm: vi.fn(),
|
|
legacyWideConfirm: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@vrtmrz/obsidian-plugin-kit", () => ({
|
|
confirmAction: mocks.confirmAction,
|
|
pickOne: mocks.pickOne,
|
|
promptPassword: mocks.promptPassword,
|
|
promptText: mocks.promptText,
|
|
}));
|
|
|
|
vi.mock("@/modules/coreObsidian/UILib/dialogs", () => ({
|
|
askSelectString: mocks.legacyAskSelectString,
|
|
askString: mocks.legacyAskString,
|
|
askYesNo: mocks.legacyAskYesNo,
|
|
confirmWithMessage: mocks.legacyConfirm,
|
|
confirmWithMessageWithWideButton: mocks.legacyWideConfirm,
|
|
}));
|
|
|
|
vi.mock("@/deps", () => ({
|
|
Notice: class {},
|
|
}));
|
|
|
|
import { EVENT_PLUGIN_UNLOADED } from "@/common/events";
|
|
import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context";
|
|
import { ObsidianConfirm } from "./ObsidianConfirm";
|
|
import type { ObsidianServiceContext } from "./ObsidianServiceContext";
|
|
|
|
function createConfirm() {
|
|
const app = { id: "app" };
|
|
const plugin = { app };
|
|
const events = createLiveSyncEventHub();
|
|
const notices = {
|
|
show: vi.fn(),
|
|
hide: vi.fn(),
|
|
};
|
|
const context = { app, plugin, events, notices } as unknown as ObsidianServiceContext;
|
|
return { confirm: new ObsidianConfirm(context), events, app, plugin, notices };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("ObsidianConfirm Fancy Kit adapter", () => {
|
|
it("uses owner-bound Kit prompts and preserves cancellation and empty input", async () => {
|
|
const { confirm, app } = createConfirm();
|
|
mocks.promptText.mockResolvedValueOnce(null);
|
|
mocks.promptPassword.mockResolvedValueOnce("");
|
|
|
|
await expect(confirm.askString("Name", "Device name", "New Remote")).resolves.toBe(false);
|
|
await expect(confirm.askString("Secret", "Passphrase", "Enter it", true)).resolves.toBe("");
|
|
|
|
expect(mocks.promptText).toHaveBeenCalledWith(
|
|
app,
|
|
{
|
|
title: "Name",
|
|
label: "Device name",
|
|
placeholder: "New Remote",
|
|
},
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.promptPassword).toHaveBeenCalledWith(
|
|
app,
|
|
{
|
|
title: "Secret",
|
|
label: "Passphrase",
|
|
placeholder: "Enter it",
|
|
},
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.legacyAskString).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses Kit for untimed yes/no and typed selection while preserving dismissed results", async () => {
|
|
const { confirm, app } = createConfirm();
|
|
mocks.confirmAction.mockResolvedValueOnce("yes");
|
|
mocks.pickOne.mockResolvedValueOnce("Beta").mockResolvedValueOnce(null);
|
|
|
|
await expect(confirm.askYesNo("Continue?")).resolves.toBe("yes");
|
|
await expect(confirm.askSelectString("Target", ["Alpha", "Beta"])).resolves.toBe("Beta");
|
|
await expect(confirm.askSelectString("Target", ["Alpha"])).resolves.toBe("");
|
|
|
|
expect(mocks.confirmAction).toHaveBeenCalledWith(
|
|
app,
|
|
expect.objectContaining({
|
|
message: "Continue?",
|
|
actions: ["yes", "no"],
|
|
actionLayout: "vertical",
|
|
defaultAction: "no",
|
|
}),
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.pickOne).toHaveBeenCalledWith(
|
|
app,
|
|
expect.objectContaining({
|
|
items: ["Alpha", "Beta"],
|
|
getText: expect.any(Function),
|
|
}),
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.legacyAskYesNo).not.toHaveBeenCalled();
|
|
expect(mocks.legacyAskSelectString).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps untimed and countdown action dialogues vertically stacked", async () => {
|
|
const { confirm, app, plugin } = createConfirm();
|
|
mocks.confirmAction.mockResolvedValueOnce("Apply").mockResolvedValueOnce("Yes");
|
|
mocks.legacyWideConfirm.mockResolvedValueOnce("Cancel").mockResolvedValueOnce("No");
|
|
|
|
await expect(confirm.confirmWithMessage("Review", "**Apply?**", ["Apply", "Cancel"], "Cancel")).resolves.toBe(
|
|
"Apply"
|
|
);
|
|
await expect(confirm.askYesNoDialog("Continue?", { title: "Question", defaultOption: "Yes" })).resolves.toBe(
|
|
"yes"
|
|
);
|
|
await expect(confirm.confirmWithMessage("Timed", "Wait", ["Apply", "Cancel"], "Cancel", 30)).resolves.toBe(
|
|
"Cancel"
|
|
);
|
|
await expect(confirm.askYesNoDialog("Timed?", { defaultOption: "No", timeout: 10 })).resolves.toBe("no");
|
|
|
|
expect(mocks.confirmAction).toHaveBeenNthCalledWith(
|
|
1,
|
|
app,
|
|
{
|
|
title: "Review",
|
|
message: "**Apply?**",
|
|
actions: ["Apply", "Cancel"],
|
|
actionLayout: "vertical",
|
|
defaultAction: "Cancel",
|
|
sourcePath: "/",
|
|
},
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.confirmAction).toHaveBeenNthCalledWith(
|
|
2,
|
|
app,
|
|
expect.objectContaining({
|
|
title: "Question",
|
|
message: "Continue?",
|
|
actions: ["Yes", "No"],
|
|
actionLayout: "vertical",
|
|
defaultAction: "Yes",
|
|
}),
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.legacyWideConfirm).toHaveBeenNthCalledWith(
|
|
1,
|
|
plugin,
|
|
"Timed",
|
|
"Wait",
|
|
["Apply", "Cancel"],
|
|
"Cancel",
|
|
30
|
|
);
|
|
expect(mocks.legacyWideConfirm).toHaveBeenNthCalledWith(
|
|
2,
|
|
plugin,
|
|
expect.any(String),
|
|
"Timed?",
|
|
expect.any(Array),
|
|
expect.any(String),
|
|
10
|
|
);
|
|
expect(mocks.legacyConfirm).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses Kit for untimed multi-action selection and keeps timed wide actions on the countdown dialogue", async () => {
|
|
const { confirm, app, plugin } = createConfirm();
|
|
const actions = ["Apply now", "Review later"] as const;
|
|
mocks.confirmAction.mockResolvedValueOnce("Review later");
|
|
mocks.legacyWideConfirm.mockResolvedValueOnce("Apply now");
|
|
|
|
await expect(
|
|
confirm.askSelectStringDialogue("Choose the next step", actions, {
|
|
title: "Next step",
|
|
defaultAction: "Review later",
|
|
})
|
|
).resolves.toBe("Review later");
|
|
await expect(
|
|
confirm.askSelectStringDialogue("Choose before the timer expires", actions, {
|
|
title: "Timed step",
|
|
defaultAction: "Apply now",
|
|
timeout: 15,
|
|
})
|
|
).resolves.toBe("Apply now");
|
|
|
|
expect(mocks.confirmAction).toHaveBeenCalledWith(
|
|
app,
|
|
{
|
|
title: "Next step",
|
|
message: "Choose the next step",
|
|
actions,
|
|
actionLayout: "vertical",
|
|
defaultAction: "Review later",
|
|
sourcePath: "/",
|
|
},
|
|
{ signal: expect.any(AbortSignal) }
|
|
);
|
|
expect(mocks.legacyWideConfirm).toHaveBeenCalledWith(
|
|
plugin,
|
|
"Timed step",
|
|
"Choose before the timer expires",
|
|
actions,
|
|
"Apply now",
|
|
15
|
|
);
|
|
});
|
|
|
|
it("refers P2P connection approvals to the current P2P Status pane", async () => {
|
|
const { confirm, plugin } = createConfirm();
|
|
const actions = ["Accept", "Ignore"] as const;
|
|
mocks.legacyWideConfirm.mockResolvedValueOnce("Ignore");
|
|
|
|
await confirm.askSelectStringDialogue(
|
|
"You can revoke your decision from the Peer-to-Peer Replicator Pane.",
|
|
actions,
|
|
{
|
|
title: "P2P Connection Request",
|
|
defaultAction: "Ignore",
|
|
timeout: 30,
|
|
}
|
|
);
|
|
|
|
expect(mocks.legacyWideConfirm).toHaveBeenCalledWith(
|
|
plugin,
|
|
"P2P Connection Request",
|
|
"You can revoke your decision from the P2P Status pane.",
|
|
actions,
|
|
"Ignore",
|
|
30
|
|
);
|
|
});
|
|
|
|
it("dismisses an open Kit dialogue when the plug-in unload event is emitted", async () => {
|
|
const { confirm, events } = createConfirm();
|
|
let observedSignal: AbortSignal | undefined;
|
|
mocks.confirmAction.mockImplementation(
|
|
(_app, _options, lifecycle: { signal: AbortSignal }) =>
|
|
new Promise<null>((resolve) => {
|
|
observedSignal = lifecycle.signal;
|
|
lifecycle.signal.addEventListener("abort", () => resolve(null), { once: true });
|
|
})
|
|
);
|
|
|
|
const result = confirm.confirmWithMessage("Review", "Message", ["OK"], "OK");
|
|
expect(observedSignal?.aborted).toBe(false);
|
|
|
|
events.emitEvent(EVENT_PLUGIN_UNLOADED);
|
|
|
|
await expect(result).resolves.toBe(false);
|
|
expect(observedSignal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("routes popup display and expiry through the context-owned keyed Notice manager", async () => {
|
|
vi.useFakeTimers();
|
|
const popupKey = "popup-remote-size-exceeded";
|
|
const fragment = {} as DocumentFragment;
|
|
const anchor = { addEventListener: vi.fn() } as unknown as HTMLAnchorElement;
|
|
const span = {
|
|
appendText: vi.fn(),
|
|
appendChild: vi.fn(),
|
|
createEl: vi.fn((_tag, _options, callback: (element: HTMLAnchorElement) => void) => {
|
|
callback(anchor);
|
|
return anchor;
|
|
}),
|
|
};
|
|
vi.stubGlobal("createFragment", (callback: (document: unknown) => void) => {
|
|
callback({
|
|
createSpan: (_options: unknown, build: (element: typeof span) => void) => build(span),
|
|
});
|
|
return fragment;
|
|
});
|
|
const { confirm, notices } = createConfirm();
|
|
|
|
try {
|
|
confirm.askInPopup("remote-size-exceeded", "Review {HERE} details", vi.fn(), 20_000);
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
|
|
expect(notices.show).toHaveBeenCalledWith(popupKey, fragment, { durationMs: 20_000 });
|
|
} finally {
|
|
await vi.runAllTimersAsync();
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
}
|
|
});
|
|
|
|
it("closes an owned keyed Notice when the plug-in unload event is emitted", () => {
|
|
const { confirm, events, notices } = createConfirm();
|
|
const popupKey = "popup-remote-size-exceeded";
|
|
(confirm as unknown as { popupKeys: Set<string> }).popupKeys.add(popupKey);
|
|
|
|
events.emitEvent(EVENT_PLUGIN_UNLOADED);
|
|
|
|
expect(notices.hide).toHaveBeenCalledWith(popupKey);
|
|
});
|
|
});
|