mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 13:02:58 +00:00
Prepare the 1.0 compatibility review flow
This commit is contained in:
@@ -188,6 +188,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
override onOpen() {
|
||||
this.component.load();
|
||||
const { contentEl } = this;
|
||||
contentEl.closest(".modal-container")?.classList.add("livesync-message-box-container");
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
div.setCssStyles({
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
REMOTE_P2P,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { delay, isObjectDifferent, sizeToHumanReadable } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
import { versionNumberString2Number } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/convert";
|
||||
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { checkSyncInfo } from "@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation";
|
||||
import { testCrypt } from "octagonal-wheels/encryption/encryption";
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
|
||||
import { confirmWithMessage } from "@/modules/coreObsidian/UILib/dialogs.ts";
|
||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "@/common/events.ts";
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import {
|
||||
@@ -417,11 +415,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
manifestVersion: string = MANIFEST_VERSION || "-";
|
||||
|
||||
lastVersion = ~~(versionNumberString2Number(this.manifestVersion) / 1000);
|
||||
|
||||
screenElements: { [key: string]: HTMLElement[] } = {};
|
||||
changeDisplay(screen: string) {
|
||||
for (const k in this.screenElements) {
|
||||
@@ -623,7 +616,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
OPTION_ONLY_SETTING,
|
||||
OPTION_CANCEL,
|
||||
];
|
||||
const result = await confirmWithMessage(this.plugin, title, note, buttons, OPTION_CANCEL, 0);
|
||||
const result = await this.core.confirm.confirmWithMessage(title, note, buttons, OPTION_CANCEL);
|
||||
if (result == OPTION_CANCEL) return;
|
||||
if (result == OPTION_FETCH) {
|
||||
if (!(await this.checkWorkingPassphrase())) {
|
||||
@@ -829,18 +822,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
|
||||
void yieldNextAnimationFrame().then(() => {
|
||||
if (this.selectedScreen == "") {
|
||||
if (this.lastVersion != this.editingSettings.lastReadUpdates) {
|
||||
if (this.editingSettings.isConfigured) {
|
||||
changeDisplay("100");
|
||||
} else {
|
||||
changeDisplay("110");
|
||||
}
|
||||
if (this.isAnySyncEnabled()) {
|
||||
changeDisplay("20");
|
||||
} else {
|
||||
if (this.isAnySyncEnabled()) {
|
||||
changeDisplay("20");
|
||||
} else {
|
||||
changeDisplay("110");
|
||||
}
|
||||
changeDisplay("110");
|
||||
}
|
||||
} else {
|
||||
changeDisplay(this.selectedScreen);
|
||||
|
||||
@@ -1,61 +1,11 @@
|
||||
import { MarkdownRenderer } from "@/deps.ts";
|
||||
import { versionNumberString2Number } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/convert";
|
||||
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
//@ts-ignore
|
||||
const manifestVersion: string = MANIFEST_VERSION || "-";
|
||||
//@ts-ignore
|
||||
const updateInformation: string = UPDATE_INFO || "";
|
||||
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void {
|
||||
const cx = this.createEl(
|
||||
paneEl,
|
||||
"div",
|
||||
{
|
||||
cls: "op-warn-info",
|
||||
},
|
||||
undefined,
|
||||
visibleOnly(() => !this.isConfiguredAs("versionUpFlash", ""))
|
||||
);
|
||||
this.createEl(
|
||||
cx,
|
||||
"div",
|
||||
{
|
||||
text: this.editingSettings.versionUpFlash,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
this.createEl(cx, "button", { text: $msg("obsidianLiveSyncSettingTab.btnGotItAndUpdated") }, (e) => {
|
||||
e.addClass("mod-cta");
|
||||
e.addEventListener("click", () => {
|
||||
fireAndForget(async () => {
|
||||
this.editingSettings.versionUpFlash = "";
|
||||
await this.saveAllDirtySettings();
|
||||
});
|
||||
});
|
||||
});
|
||||
const informationDivEl = this.createEl(paneEl, "div", { text: "" });
|
||||
const tmpDiv = createDiv();
|
||||
// tmpDiv.addClass("sls-header-button");
|
||||
tmpDiv.addClass("op-warn-info");
|
||||
|
||||
tmpDiv.createEl("p", { text: $msg("obsidianLiveSyncSettingTab.msgNewVersionNote") });
|
||||
const readEverythingButton = tmpDiv.createEl("button", {
|
||||
text: $msg("obsidianLiveSyncSettingTab.optionOkReadEverything"),
|
||||
});
|
||||
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
|
||||
const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
|
||||
readEverythingButton.addEventListener("click", () => {
|
||||
fireAndForget(async () => {
|
||||
this.editingSettings.lastReadUpdates = lastVersion;
|
||||
await this.saveAllDirtySettings();
|
||||
informationButtonDiv.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
fireAndForget(() =>
|
||||
MarkdownRenderer.render(this.plugin.app, updateInformation, informationDivEl, "/", this.lifetimeComponent)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, VER, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
EVENT_LAYOUT_READY,
|
||||
EVENT_PLUGIN_LOADED,
|
||||
@@ -8,13 +12,11 @@ import {
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import { $msg, setLang } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
||||
import { versionNumberString2Number } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/convert";
|
||||
import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { initialiseWorkerModule } from "@vrtmrz/livesync-commonlib/compat/worker/bgWorker";
|
||||
import { manifestVersion, packageVersion } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvVars";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
|
||||
export class ModuleLiveSyncMain extends AbstractModule {
|
||||
async _onLiveSyncReady() {
|
||||
@@ -97,30 +99,6 @@ export class ModuleLiveSyncMain extends AbstractModule {
|
||||
this._log($msg("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
const lsKey = "obsidian-live-sync-ver" + this.services.vault.getVaultName();
|
||||
const last_version = compatGlobal.localStorage.getItem(lsKey);
|
||||
|
||||
const lastVersion = ~~(versionNumberString2Number(manifestVersion) / 1000);
|
||||
if (lastVersion > this.settings.lastReadUpdates && this.settings.isConfigured) {
|
||||
this._log($msg("moduleLiveSyncMain.logReadChangelog"), LOG_LEVEL_NOTICE);
|
||||
}
|
||||
|
||||
// //@ts-ignore
|
||||
// if (this.isMobile) {
|
||||
// this.settings.disableRequestURI = true;
|
||||
// }
|
||||
if (last_version && Number(last_version) < VER) {
|
||||
this.settings.liveSync = false;
|
||||
this.settings.syncOnSave = false;
|
||||
this.settings.syncOnEditorSave = false;
|
||||
this.settings.syncOnStart = false;
|
||||
this.settings.syncOnFileOpen = false;
|
||||
this.settings.syncAfterMerge = false;
|
||||
this.settings.periodicReplication = false;
|
||||
this.settings.versionUpFlash = $msg("moduleLiveSyncMain.logVersionUpdate");
|
||||
await this.saveSettings();
|
||||
}
|
||||
compatGlobal.localStorage.setItem(lsKey, `${VER}`);
|
||||
await this.services.database.openDatabase({
|
||||
databaseEvents: this.services.databaseEvents,
|
||||
replicator: this.services.replicator,
|
||||
|
||||
@@ -54,6 +54,14 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:global(body.is-mobile .livesync-svelte-dialog-container .dialog-host > .button-group) {
|
||||
background: var(--modal-background, var(--background-primary));
|
||||
bottom: 0;
|
||||
padding-bottom: 1px;
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dialog-host {
|
||||
padding: 20px;
|
||||
gap: 0.5em;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { type App, type Plugin, Notice } from "@/deps";
|
||||
import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils";
|
||||
import { EVENT_PLUGIN_UNLOADED } from "@/common/events";
|
||||
import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
import type { Confirm, ConfirmActionLayout } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
import { confirmAction, pickOne, promptPassword, promptText } from "@vrtmrz/obsidian-plugin-kit";
|
||||
import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext";
|
||||
import {
|
||||
askYesNo,
|
||||
askString,
|
||||
confirmWithMessageWithWideButton,
|
||||
askSelectString,
|
||||
confirmWithMessage,
|
||||
confirmWithMessage as confirmWithLegacyMessage,
|
||||
} from "@/modules/coreObsidian/UILib/dialogs";
|
||||
|
||||
export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceContext> implements Confirm {
|
||||
private _context: T;
|
||||
private readonly dialogueController = new AbortController();
|
||||
private readonly popupKeys = new Set<string>();
|
||||
get _app(): App {
|
||||
return this._context.app;
|
||||
}
|
||||
@@ -21,12 +22,58 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
|
||||
}
|
||||
constructor(context: T) {
|
||||
this._context = context;
|
||||
context.events.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
|
||||
this.dialogueController.abort();
|
||||
for (const popupKey of this.popupKeys) {
|
||||
this.closePopup(popupKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
askYesNo(message: string): Promise<"yes" | "no"> {
|
||||
return askYesNo(this._app, message);
|
||||
|
||||
private get dialogueLifecycle() {
|
||||
return { signal: this.dialogueController.signal };
|
||||
}
|
||||
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
|
||||
return askString(this._app, title, key, placeholder, isPassword);
|
||||
|
||||
private hasCountdown(timeout: number | undefined): timeout is number {
|
||||
return timeout !== undefined && timeout > 0;
|
||||
}
|
||||
|
||||
async askYesNo(message: string): Promise<"yes" | "no"> {
|
||||
const result = await confirmAction(
|
||||
this._app,
|
||||
{
|
||||
title: $msg("moduleInputUIObsidian.defaultTitleConfirmation"),
|
||||
message,
|
||||
actions: ["yes", "no"] as const,
|
||||
labels: {
|
||||
yes: $msg("moduleInputUIObsidian.optionYes"),
|
||||
no: $msg("moduleInputUIObsidian.optionNo"),
|
||||
},
|
||||
defaultAction: "no",
|
||||
sourcePath: "/",
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result === "yes" ? "yes" : "no";
|
||||
}
|
||||
|
||||
async askString(
|
||||
title: string,
|
||||
key: string,
|
||||
placeholder: string,
|
||||
isPassword: boolean = false
|
||||
): Promise<string | false> {
|
||||
const prompt = isPassword ? promptPassword : promptText;
|
||||
const result = await prompt(
|
||||
this._app,
|
||||
{
|
||||
title,
|
||||
label: key,
|
||||
placeholder,
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
async askYesNoDialog(
|
||||
@@ -37,6 +84,21 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
|
||||
const yesLabel = $msg("moduleInputUIObsidian.optionYes");
|
||||
const noLabel = $msg("moduleInputUIObsidian.optionNo");
|
||||
const defaultOption = opt.defaultOption === "Yes" ? yesLabel : noLabel;
|
||||
if (!this.hasCountdown(opt.timeout)) {
|
||||
const result = await confirmAction(
|
||||
this._app,
|
||||
{
|
||||
title: opt.title || defaultTitle,
|
||||
message,
|
||||
actions: ["Yes", "No"] as const,
|
||||
labels: { Yes: yesLabel, No: noLabel },
|
||||
defaultAction: opt.defaultOption === "Yes" ? "Yes" : "No",
|
||||
sourcePath: "/",
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result === "Yes" ? "yes" : "no";
|
||||
}
|
||||
const ret = await confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
@@ -48,16 +110,39 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
|
||||
return ret === yesLabel ? "yes" : "no";
|
||||
}
|
||||
|
||||
askSelectString(message: string, items: string[]): Promise<string> {
|
||||
return askSelectString(this._app, message, items);
|
||||
async askSelectString(message: string, items: string[]): Promise<string> {
|
||||
const result = await pickOne(
|
||||
this._app,
|
||||
{
|
||||
items,
|
||||
getText: (item) => item,
|
||||
placeholder: message,
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result ?? "";
|
||||
}
|
||||
|
||||
askSelectStringDialogue<T extends readonly string[]>(
|
||||
async askSelectStringDialogue<T extends readonly string[]>(
|
||||
message: string,
|
||||
buttons: T,
|
||||
opt: { title?: string; defaultAction: T[number]; timeout?: number }
|
||||
): Promise<T[number] | false> {
|
||||
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
|
||||
if (!this.hasCountdown(opt.timeout)) {
|
||||
const result = await confirmAction(
|
||||
this._app,
|
||||
{
|
||||
title: opt.title || defaultTitle,
|
||||
message,
|
||||
actions: buttons,
|
||||
defaultAction: opt.defaultAction,
|
||||
sourcePath: "/",
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
return confirmWithMessageWithWideButton(
|
||||
this._plugin,
|
||||
opt.title || defaultTitle,
|
||||
@@ -68,7 +153,14 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
|
||||
);
|
||||
}
|
||||
|
||||
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) {
|
||||
askInPopup(
|
||||
key: string,
|
||||
dialogText: string,
|
||||
anchorCallback: (anchor: HTMLAnchorElement) => void,
|
||||
durationMs: number = 20000
|
||||
) {
|
||||
const popupKey = "popup-" + key;
|
||||
this.popupKeys.add(popupKey);
|
||||
const fragment = createFragment((doc) => {
|
||||
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
|
||||
doc.createSpan(undefined, (a) => {
|
||||
@@ -76,36 +168,62 @@ export class ObsidianConfirm<T extends ObsidianServiceContext = ObsidianServiceC
|
||||
a.appendChild(
|
||||
a.createEl("a", undefined, (anchor) => {
|
||||
anchorCallback(anchor);
|
||||
anchor.addEventListener("click", () => this.closePopup(popupKey));
|
||||
})
|
||||
);
|
||||
a.appendText(afterText);
|
||||
});
|
||||
});
|
||||
const popupKey = "popup-" + key;
|
||||
scheduleTask(popupKey, 1000, async () => {
|
||||
if (this.dialogueController.signal.aborted) {
|
||||
this.popupKeys.delete(popupKey);
|
||||
return;
|
||||
}
|
||||
const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0));
|
||||
const isShown = popup?.noticeEl?.isShown();
|
||||
if (!isShown) {
|
||||
memoObject(popupKey, new Notice(fragment, 0));
|
||||
}
|
||||
scheduleTask(popupKey + "-close", 20000, () => {
|
||||
const popup = retrieveMemoObject<Notice>(popupKey);
|
||||
if (!popup) return;
|
||||
if (popup?.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(popupKey);
|
||||
});
|
||||
scheduleTask(popupKey + "-close", durationMs, () => this.closePopup(popupKey));
|
||||
});
|
||||
}
|
||||
|
||||
confirmWithMessage(
|
||||
private closePopup(popupKey: string) {
|
||||
const popup = retrieveMemoObject<Notice>(popupKey);
|
||||
if (!popup) {
|
||||
this.popupKeys.delete(popupKey);
|
||||
return;
|
||||
}
|
||||
if (popup.noticeEl?.isShown()) {
|
||||
popup.hide();
|
||||
}
|
||||
disposeMemoObject(popupKey);
|
||||
this.popupKeys.delete(popupKey);
|
||||
}
|
||||
|
||||
async confirmWithMessage(
|
||||
title: string,
|
||||
contentMd: string,
|
||||
buttons: string[],
|
||||
defaultAction: (typeof buttons)[number],
|
||||
timeout?: number
|
||||
timeout?: number,
|
||||
actionLayout?: ConfirmActionLayout
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout);
|
||||
if (this.hasCountdown(timeout)) {
|
||||
return confirmWithLegacyMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout);
|
||||
}
|
||||
const result = await confirmAction(
|
||||
this._app,
|
||||
{
|
||||
title,
|
||||
message: contentMd,
|
||||
actions: buttons,
|
||||
defaultAction,
|
||||
sourcePath: "/",
|
||||
...(actionLayout === undefined ? {} : { actionLayout }),
|
||||
},
|
||||
this.dialogueLifecycle
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
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 { memoObject, retrieveMemoObject } from "@/common/utils";
|
||||
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 context = { app, plugin, events } as unknown as ObsidianServiceContext;
|
||||
return { confirm: new ObsidianConfirm(context), events, app, plugin };
|
||||
}
|
||||
|
||||
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"],
|
||||
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("uses Kit for untimed action dialogues and retains the legacy countdown path", async () => {
|
||||
const { confirm, app, plugin } = createConfirm();
|
||||
mocks.confirmAction.mockResolvedValueOnce("Apply").mockResolvedValueOnce("Yes");
|
||||
mocks.legacyConfirm.mockResolvedValueOnce("Cancel");
|
||||
mocks.legacyWideConfirm.mockResolvedValueOnce("No");
|
||||
|
||||
await expect(
|
||||
confirm.confirmWithMessage("Review", "**Apply?**", ["Apply", "Cancel"], "Cancel", undefined, "vertical")
|
||||
).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.legacyConfirm).toHaveBeenCalledWith(plugin, "Timed", "Wait", ["Apply", "Cancel"], "Cancel", 30);
|
||||
expect(mocks.legacyWideConfirm).toHaveBeenCalledWith(
|
||||
plugin,
|
||||
expect.any(String),
|
||||
"Timed?",
|
||||
expect.any(Array),
|
||||
expect.any(String),
|
||||
10
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
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("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("closes an active Notice when the plug-in unload event is emitted", () => {
|
||||
const { confirm, events } = createConfirm();
|
||||
const popupKey = "popup-remote-size-exceeded";
|
||||
const popup = {
|
||||
hide: vi.fn(),
|
||||
noticeEl: { isShown: vi.fn(() => true) },
|
||||
};
|
||||
memoObject(popupKey, popup);
|
||||
(confirm as unknown as { popupKeys: Set<string> }).popupKeys.add(popupKey);
|
||||
|
||||
events.emitEvent(EVENT_PLUGIN_UNLOADED);
|
||||
|
||||
expect(popup.hide).toHaveBeenCalledOnce();
|
||||
expect(retrieveMemoObject(popupKey)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user