mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-06 00:08:23 +00:00
Merge remote-tracking branch 'upstream/main' into feat/syncguard-bulk-sync-edit-guard
# Conflicts: # src/common/messages/LiveSyncProvisionalMessages.ts # styles.css # updates.md
This commit is contained in:
commit
acd3b0ade6
157 files changed
+15431
-1108
No files matched your search
@@ -23,6 +23,7 @@ export function createAdvancedSettingSpecGroups({
|
||||
control: { type: "dropdown", options: () => ChunkAlgorithmNames },
|
||||
},
|
||||
{ key: "customChunkSize", control: { type: "number", min: 0, allowZero: true } },
|
||||
{ key: "strictContentCheckOnStartup", control: { type: "toggle" } },
|
||||
{
|
||||
key: "pathNormalizationVersion",
|
||||
holdValue: true,
|
||||
|
||||
+8
-6
@@ -1,3 +1,4 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingDefinitionGroup, SettingDefinitionItem, SettingDefinitionPage } from "obsidian";
|
||||
@@ -158,6 +159,7 @@ function createSettingsTab(options: SettingsTabOptions = {}): ObsidianLiveSyncSe
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
saveSettingData: vi.fn(async () => undefined),
|
||||
},
|
||||
@@ -459,7 +461,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
expect(runtime.pageCleanup).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue when an externally loaded setting changes page visibility", () => {
|
||||
it("rebuilds the catalogue when an externally loaded setting changes page visibility", async () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
@@ -468,7 +470,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
changeLog.page().display();
|
||||
tab.core.settings.usePowerUserMode = !tab.editingSettings.usePowerUserMode;
|
||||
|
||||
tab.requestReload();
|
||||
await tab.requestReload();
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -482,7 +484,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds translated catalogue names when the display language changes externally", () => {
|
||||
it("rebuilds translated catalogue names when the display language changes externally", async () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
@@ -491,12 +493,12 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
changeLog.page().display();
|
||||
tab.core.settings.displayLanguage = "ja";
|
||||
|
||||
tab.requestReload();
|
||||
await tab.requestReload();
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue after accepting an external page-visibility setting over a dirty value", () => {
|
||||
it("rebuilds the catalogue after accepting an external page-visibility setting over a dirty value", async () => {
|
||||
const tab = createSettingsTab();
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
@@ -507,7 +509,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
tab.editingSettings.usePowerUserMode = true;
|
||||
tab.core.settings.usePowerUserMode = true;
|
||||
|
||||
tab.requestReload();
|
||||
await tab.requestReload();
|
||||
const configureAnchor = vi.mocked(tab.core.confirm.askInPopup).mock.calls[0]?.[2];
|
||||
expect(configureAnchor).toBeTypeOf("function");
|
||||
let acceptExternalSetting: (() => void) | undefined;
|
||||
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
type PageFunctions,
|
||||
type UpdateFunction,
|
||||
} from "./SettingPane.ts";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { JournalSyncCore } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncCore";
|
||||
import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replication/journal/objectstore/MinioStorageAdapter";
|
||||
import { closeObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||
@@ -100,6 +99,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
selectedScreen = "";
|
||||
|
||||
private localConfigurationPassphrase = "";
|
||||
private localSettingsReadVersion = 0;
|
||||
|
||||
_editingSettings?: AllSettings;
|
||||
// Buffered Settings for editing
|
||||
get editingSettings(): AllSettings {
|
||||
@@ -162,8 +164,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
async saveLocalSetting(key: keyof typeof OnDialogSettingsDefault) {
|
||||
if (key == "deviceAndVaultName") {
|
||||
this.services.setting.setDeviceAndVaultName(this.editingSettings?.[key] ?? "");
|
||||
this.services.setting.saveDeviceAndVaultName();
|
||||
return await Promise.resolve();
|
||||
await this.services.setting.saveDeviceAndVaultName();
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -225,6 +226,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
appliedConfigurationEncryption.configPassphrase
|
||||
);
|
||||
|
||||
if (appliedConfigurationEncryption.configPassphraseStore === "LOCALSTORAGE") {
|
||||
this.localSettingsReadVersion++;
|
||||
this.localConfigurationPassphrase = appliedConfigurationEncryption.configPassphrase;
|
||||
}
|
||||
this.copySettingValue(this.initialSettings, appliedConfigurationEncryption, "configPassphraseStore");
|
||||
this.copySettingValue(this.initialSettings, appliedConfigurationEncryption, "configPassphrase");
|
||||
}
|
||||
@@ -285,9 +290,21 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
|
||||
private async readLocalSettings(): Promise<boolean> {
|
||||
const version = ++this.localSettingsReadVersion;
|
||||
const passphrase = await this.services.setting.deviceValues.getValue("ls-setting-passphrase", "");
|
||||
if (version !== this.localSettingsReadVersion) return false;
|
||||
this.localConfigurationPassphrase = passphrase;
|
||||
return true;
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
if (await this.readLocalSettings()) this.reloadAllSettings(true);
|
||||
}
|
||||
|
||||
reloadAllLocalSettings() {
|
||||
const ret = { ...OnDialogSettingsDefault };
|
||||
ret.configPassphrase = compatGlobal.localStorage.getItem("ls-setting-passphrase") || "";
|
||||
ret.configPassphrase = this.localConfigurationPassphrase;
|
||||
ret.preset = "";
|
||||
ret.deviceAndVaultName = this.services.setting.getDeviceAndVaultName();
|
||||
return ret;
|
||||
@@ -402,7 +419,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
this.plugin = plugin;
|
||||
Setting.env = this;
|
||||
eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, () => {
|
||||
this.requestReload();
|
||||
void this.requestReload().catch((error: unknown) => Logger(error, LOG_LEVEL_NOTICE));
|
||||
});
|
||||
this.addOnSaved("displayLanguage", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("showStatusOnEditor", () => eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR));
|
||||
@@ -545,7 +562,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
}
|
||||
|
||||
requestReload() {
|
||||
async requestReload(): Promise<void> {
|
||||
if (!(await this.readLocalSettings())) return;
|
||||
const nativeTabIsShown =
|
||||
this.supportsDeclarativeSettings() && this.containerEl !== undefined && this.containerEl.isShown();
|
||||
if (this.isShown || nativeTabIsShown) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { REMOTE_RESOURCE_KINDS } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
|
||||
const settingsInitialisationMocks = vi.hoisted(() => ({
|
||||
applySettingsWithInitialisationChoice: vi.fn(),
|
||||
@@ -76,8 +76,6 @@ beforeEach(() => {
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockReset();
|
||||
loggerMocks.Logger.mockClear();
|
||||
vi.mocked($msg).mockClear();
|
||||
vi.mocked(compatGlobal.localStorage.getItem).mockReset().mockReturnValue(null);
|
||||
vi.mocked(compatGlobal.localStorage.setItem).mockReset();
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
@@ -280,6 +278,7 @@ describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
saveSettingData,
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
suspendAllSync,
|
||||
@@ -665,6 +664,7 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
saveSettingData,
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
},
|
||||
@@ -692,6 +692,54 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
return { tab, saveSettingData };
|
||||
}
|
||||
|
||||
it("awaits the stored passphrase before initialising the editing buffer", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
tab.resetEditingSettings();
|
||||
let resolveRead!: (value: string) => void;
|
||||
vi.spyOn(tab.services.setting.deviceValues, "getValue").mockReturnValueOnce(
|
||||
new Promise<string>((resolve) => {
|
||||
resolveRead = resolve;
|
||||
})
|
||||
);
|
||||
const loading = tab.loadSettings();
|
||||
expect(tab.initialSettings).toBeUndefined();
|
||||
resolveRead("stored passphrase");
|
||||
await loading;
|
||||
expect(tab.editingSettings.configPassphrase).toBe("stored passphrase");
|
||||
expect(tab.initialSettings!.configPassphrase).toBe("stored passphrase");
|
||||
});
|
||||
|
||||
it("preserves the loaded settings when device storage cannot be read", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
await tab.services.setting.deviceValues.setValue("ls-setting-passphrase", "stored passphrase");
|
||||
await tab.loadSettings();
|
||||
const failure = new Error("device storage unavailable");
|
||||
vi.spyOn(tab.services.setting.deviceValues, "getValue").mockRejectedValueOnce(failure);
|
||||
await expect(tab.requestReload()).rejects.toBe(failure);
|
||||
expect(tab.editingSettings.configPassphrase).toBe("stored passphrase");
|
||||
expect(tab.initialSettings!.configPassphrase).toBe("stored passphrase");
|
||||
});
|
||||
|
||||
it("does not let a delayed read overwrite a successfully saved passphrase", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
await tab.loadSettings();
|
||||
let resolveRead!: (value: string) => void;
|
||||
vi.spyOn(tab.services.setting.deviceValues, "getValue").mockReturnValueOnce(
|
||||
new Promise<string>((resolve) => {
|
||||
resolveRead = resolve;
|
||||
})
|
||||
);
|
||||
const loading = tab.loadSettings();
|
||||
Object.assign(tab.services.setting, { changeConfigurationEncryption: vi.fn(async () => undefined) });
|
||||
tab.editingSettings.configPassphraseStore = "LOCALSTORAGE";
|
||||
tab.editingSettings.configPassphrase = "new passphrase";
|
||||
await tab.saveSettings(["configPassphraseStore", "configPassphrase"]);
|
||||
resolveRead("old passphrase");
|
||||
await loading;
|
||||
tab.reloadAllSettings();
|
||||
expect(tab.initialSettings!.configPassphrase).toBe("new passphrase");
|
||||
});
|
||||
|
||||
it("loads the imperative fallback without a SettingPage runtime export", () => {
|
||||
const { tab } = createSettingsTab();
|
||||
|
||||
@@ -779,7 +827,7 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
it("saves a custom configuration passphrase edit through the setting service", async () => {
|
||||
const { tab } = createSettingsTab();
|
||||
const changeConfigurationEncryption = vi.fn(async () => {
|
||||
vi.mocked(compatGlobal.localStorage.getItem).mockReturnValue("new custom passphrase");
|
||||
await tab.services.setting.deviceValues.setValue("ls-setting-passphrase", "new custom passphrase");
|
||||
});
|
||||
Object.assign(tab.core.services.setting, { changeConfigurationEncryption });
|
||||
const store = "LOCALSTORAGE";
|
||||
@@ -787,13 +835,13 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
tab.initialSettings!.configPassphraseStore = store;
|
||||
tab.editingSettings.configPassphrase = "new custom passphrase";
|
||||
tab.initialSettings!.configPassphrase = "old custom passphrase";
|
||||
vi.mocked(compatGlobal.localStorage.setItem).mockClear();
|
||||
const setValue = vi.spyOn(tab.services.setting.deviceValues, "setValue");
|
||||
|
||||
await tab.saveSettings(["configPassphrase"]);
|
||||
|
||||
expect(changeConfigurationEncryption).toHaveBeenCalledOnce();
|
||||
expect(changeConfigurationEncryption).toHaveBeenCalledWith(store, "new custom passphrase");
|
||||
expect(compatGlobal.localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(setValue).toHaveBeenCalledOnce();
|
||||
expect(tab.initialSettings!.configPassphrase).toBe("new custom passphrase");
|
||||
});
|
||||
|
||||
@@ -811,7 +859,6 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
tab.initialSettings!.configPassphraseStore = "LOCALSTORAGE";
|
||||
tab.initialSettings!.configPassphrase = "previous passphrase";
|
||||
tab.editingSettings.configPassphrase = "passphrase A";
|
||||
vi.mocked(compatGlobal.localStorage.getItem).mockReturnValue("passphrase A");
|
||||
|
||||
const apply = tab.saveSettings(["configPassphrase"]);
|
||||
expect(changeConfigurationEncryption).toHaveBeenCalledWith("LOCALSTORAGE", "passphrase A");
|
||||
@@ -840,13 +887,13 @@ describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
tab.editingSettings.configPassphraseStore = editedStore;
|
||||
tab.initialSettings!.configPassphrase = "old custom passphrase";
|
||||
tab.editingSettings.configPassphrase = "new custom passphrase";
|
||||
vi.mocked(compatGlobal.localStorage.setItem).mockClear();
|
||||
const setValue = vi.spyOn(tab.services.setting.deviceValues, "setValue");
|
||||
|
||||
await expect(tab.saveSettings(["configPassphraseStore", "configPassphrase"])).rejects.toBe(failure);
|
||||
|
||||
expect(changeConfigurationEncryption).toHaveBeenCalledOnce();
|
||||
expect(changeConfigurationEncryption).toHaveBeenCalledWith(editedStore, "new custom passphrase");
|
||||
expect(compatGlobal.localStorage.setItem).not.toHaveBeenCalled();
|
||||
expect(setValue).not.toHaveBeenCalled();
|
||||
expect(tab.core.settings.configPassphraseStore).toBe(savedStore);
|
||||
expect(tab.initialSettings!.configPassphraseStore).toBe(savedStore);
|
||||
expect(tab.initialSettings!.configPassphrase).toBe("old custom passphrase");
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("paneAdvanced", () => {
|
||||
};
|
||||
|
||||
paneAdvanced.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(10));
|
||||
await vi.waitFor(() => expect(settingHarness.rendered).toHaveLength(11));
|
||||
|
||||
expect(addPanel.mock.calls.map(([, heading]) => heading)).toEqual([
|
||||
"Memory cache",
|
||||
@@ -56,6 +56,7 @@ describe("paneAdvanced", () => {
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Local Database Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
panelElements.get("Transfer Tweak"),
|
||||
@@ -67,6 +68,7 @@ describe("paneAdvanced", () => {
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { OPEN_JOINED_DEVICES } from "@/serviceFeatures/joinedDevicesCommand";
|
||||
import { Menu, type ButtonComponent } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
@@ -101,6 +102,22 @@ export function paneRemoteConfig(
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Joined devices"))
|
||||
.setDesc($msg("Review device records for the active CouchDB or Object Storage connection."))
|
||||
.addButton((button) =>
|
||||
button.setButtonText($msg("Review devices")).onClick(async () => {
|
||||
const result = await this.services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined);
|
||||
if (result.status === "unavailable" || result.value === "unavailable") {
|
||||
this.services.API.addLog(
|
||||
$msg(
|
||||
"Joined devices are available after database startup with an active CouchDB or Object Storage connection."
|
||||
),
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
{
|
||||
/* E2EE */
|
||||
const E2EEInitialProps = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtime = vi.hoisted(() => ({
|
||||
buttonClasses: [] as string[],
|
||||
clickHandlers: [] as Array<() => Promise<void> | void>,
|
||||
clickHandlers: [] as Array<{ setting: string; run: () => Promise<void> | void }>,
|
||||
panels: [] as Array<{ destroy: ReturnType<typeof vi.fn> }>,
|
||||
settingClasses: [] as string[],
|
||||
}));
|
||||
@@ -32,7 +32,10 @@ vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
},
|
||||
};
|
||||
|
||||
setName() {
|
||||
settingName = "";
|
||||
|
||||
setName(name: string) {
|
||||
this.settingName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -41,6 +44,7 @@ vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
}
|
||||
|
||||
addButton(callback: (button: unknown) => void) {
|
||||
const setting = this.settingName;
|
||||
const button = {
|
||||
buttonEl: {
|
||||
classList: {
|
||||
@@ -53,7 +57,7 @@ vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
return this;
|
||||
},
|
||||
onClick(callback: () => Promise<void> | void) {
|
||||
runtime.clickHandlers.push(callback);
|
||||
runtime.clickHandlers.push({ setting, run: callback });
|
||||
return this;
|
||||
},
|
||||
setButtonText() {
|
||||
@@ -107,6 +111,7 @@ vi.mock("./remoteConfigBuffer.ts", () => ({
|
||||
syncActivatedRemoteSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
import { OPEN_JOINED_DEVICES } from "@/serviceFeatures/joinedDevicesCommand";
|
||||
import { paneRemoteConfig } from "./PaneRemoteConfig.ts";
|
||||
|
||||
function createPanelElement(): HTMLElement {
|
||||
@@ -124,6 +129,18 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("paneRemoteConfig", () => {
|
||||
it("opens joined devices through the shared UI command", async () => {
|
||||
const invoke = vi.fn(async () => ({ status: "handled", value: "closed" }));
|
||||
const host = {
|
||||
editingSettings: { remoteConfigurations: {} },
|
||||
services: { UI: { commands: { invoke } } },
|
||||
};
|
||||
const addPanel = vi.fn(() => ({ then: vi.fn() }));
|
||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await runtime.clickHandlers.find((handler) => handler.setting === "Joined devices")!.run();
|
||||
expect(invoke).toHaveBeenCalledWith(OPEN_JOINED_DEVICES, undefined);
|
||||
});
|
||||
|
||||
it("destroys the E2EE info panel when the settings page lifetime unloads", async () => {
|
||||
const callbacks: Array<() => unknown> = [];
|
||||
const lifetimeComponent = {
|
||||
@@ -187,7 +204,7 @@ describe("paneRemoteConfig", () => {
|
||||
}));
|
||||
|
||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await runtime.clickHandlers[0]();
|
||||
await runtime.clickHandlers.find((handler) => handler.setting === "Configure E2EE")!.run();
|
||||
|
||||
expect(setupManager.onlyE2EEConfiguration).toHaveBeenCalledOnce();
|
||||
expect(host.editingSettings.encryptInternalMetadata).toBe(true);
|
||||
@@ -233,7 +250,7 @@ describe("paneRemoteConfig", () => {
|
||||
}));
|
||||
|
||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await runtime.clickHandlers[0]();
|
||||
await runtime.clickHandlers.find((handler) => handler.setting === "Configure E2EE")!.run();
|
||||
|
||||
expect(host.editingSettings.idDerivationVersion).toBe(1);
|
||||
expect(host.editingSettings.idDerivationKey).toBe(nextIdKey);
|
||||
|
||||
@@ -13,7 +13,7 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
.setName($msg("Synchronising files"))
|
||||
.setDesc(
|
||||
$msg(
|
||||
"(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."
|
||||
"(RegExp) Limit normal files to matching paths. Leave empty to allow all normal file paths. Hidden File Sync uses its own target patterns."
|
||||
)
|
||||
);
|
||||
const syncFilesControl = mount(MultipleRegExpControl, {
|
||||
@@ -32,9 +32,7 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
|
||||
|
||||
const nonSyncFilesSetting = new Setting(paneEl)
|
||||
.setName($msg("Non-Synchronising files"))
|
||||
.setDesc(
|
||||
$msg("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
|
||||
);
|
||||
.setDesc($msg("(RegExp) Skip changes to matching normal and hidden files, locally and remotely."));
|
||||
|
||||
const nonSyncFilesControl = mount(MultipleRegExpControl, {
|
||||
target: nonSyncFilesSetting.controlEl,
|
||||
|
||||
@@ -171,20 +171,18 @@ export function paneSyncSettings(
|
||||
}
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
paneEl,
|
||||
$msg("obsidianLiveSyncSettingTab.titleUpdateThinning"),
|
||||
undefined,
|
||||
visibleOnly(() => !this.isConfiguredAs("syncMode", "LIVESYNC"))
|
||||
).then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("batchSave");
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleUpdateThinning")).then((paneEl) => {
|
||||
const inLiveSyncMode = () => this.isConfiguredAs("syncMode", "LIVESYNC");
|
||||
const batchEnabled = () => this.isConfiguredAs(inLiveSyncMode() ? "batchSaveInLiveSync" : "batchSave", true);
|
||||
new Setting(paneEl).autoWireToggle("batchSave", { onUpdate: visibleOnly(() => !inLiveSyncMode()) });
|
||||
new Setting(paneEl).autoWireToggle("batchSaveInLiveSync", { onUpdate: visibleOnly(inLiveSyncMode) });
|
||||
new Setting(paneEl).autoWireNumeric("batchSaveMinimumDelay", {
|
||||
acceptZero: true,
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
|
||||
onUpdate: visibleOnly(batchEnabled),
|
||||
});
|
||||
new Setting(paneEl).autoWireNumeric("batchSaveMaximumDelay", {
|
||||
acceptZero: true,
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("batchSave", true)),
|
||||
onUpdate: visibleOnly(batchEnabled),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
import type { OnUpdateFunc, PageFunctions } from "./SettingPane";
|
||||
|
||||
const controls = vi.hoisted(() => new Map<string, { onUpdate?: () => { visibility?: boolean } }>());
|
||||
vi.mock("@/common/translation", () => ({ $msg: (key: string) => key }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/logger", () => ({ Logger: vi.fn() }));
|
||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||
LiveSyncSetting: class {
|
||||
autoWireDropDown(key: string, options = {}) {
|
||||
controls.set(key, options);
|
||||
return this;
|
||||
}
|
||||
autoWireToggle(key: string, options = {}) {
|
||||
controls.set(key, options);
|
||||
return this;
|
||||
}
|
||||
autoWireNumeric(key: string, options = {}) {
|
||||
controls.set(key, options);
|
||||
return this;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import { paneSyncSettings } from "./PaneSyncSettings";
|
||||
|
||||
function setup(liveSync: boolean, batchSave: boolean, batchSaveInLiveSync: boolean) {
|
||||
const callbacks = new Map<string, (value: string) => Promise<void>>();
|
||||
const panels = new Map<string, OnUpdateFunc | undefined>();
|
||||
const tab = {
|
||||
editingSettings: { ...DEFAULT_SETTINGS, liveSync, batchSave, batchSaveInLiveSync },
|
||||
services: { API: { isMobile: () => false }, control: { applySettings: vi.fn(async () => {}) } },
|
||||
addOnSaved: (key: string, callback: (value: string) => Promise<void>) => callbacks.set(key, callback),
|
||||
isConfiguredAs(key: string, value: unknown) {
|
||||
if (key === "syncMode") {
|
||||
return (
|
||||
(this.editingSettings.liveSync
|
||||
? "LIVESYNC"
|
||||
: this.editingSettings.periodicReplication
|
||||
? "PERIODIC"
|
||||
: "ONEVENTS") === value
|
||||
);
|
||||
}
|
||||
return this.editingSettings[key as keyof typeof this.editingSettings] === value;
|
||||
},
|
||||
saveSettings: vi.fn(async () => {}),
|
||||
requestCatalogueRefresh: vi.fn(),
|
||||
};
|
||||
const functions = {
|
||||
addPanel: (_element: HTMLElement, title: string, _callback: unknown, onUpdate?: OnUpdateFunc) => ({
|
||||
then(callback: (element: HTMLElement) => void) {
|
||||
if (!title.endsWith("titleSynchronizationMethod") && !title.endsWith("titleUpdateThinning")) return;
|
||||
panels.set(title, onUpdate);
|
||||
callback({} as HTMLElement);
|
||||
},
|
||||
}),
|
||||
} as unknown as PageFunctions;
|
||||
paneSyncSettings.call(tab as unknown as ObsidianLiveSyncSettingTab, {} as HTMLElement, functions);
|
||||
return { tab, callbacks, panels };
|
||||
}
|
||||
|
||||
function visible(key: string): boolean {
|
||||
const control = controls.get(key);
|
||||
if (!control) throw new Error(`Missing setting control: ${key}`);
|
||||
return control.onUpdate?.().visibility !== false;
|
||||
}
|
||||
|
||||
beforeEach(() => controls.clear());
|
||||
|
||||
describe("mode-specific database batching settings", () => {
|
||||
it.each([
|
||||
{ liveSync: true, batchSave: true, batchSaveInLiveSync: false, delays: false },
|
||||
{ liveSync: true, batchSave: false, batchSaveInLiveSync: true, delays: true },
|
||||
{ liveSync: false, batchSave: true, batchSaveInLiveSync: false, delays: true },
|
||||
{ liveSync: false, batchSave: false, batchSaveInLiveSync: true, delays: false },
|
||||
])("shows the current mode choice and its shared delays: %j", ({ delays, ...settings }) => {
|
||||
const { panels } = setup(settings.liveSync, settings.batchSave, settings.batchSaveInLiveSync);
|
||||
expect(panels.get("obsidianLiveSyncSettingTab.titleUpdateThinning")?.().visibility).not.toBe(false);
|
||||
expect(visible("batchSave")).toBe(!settings.liveSync);
|
||||
expect(visible("batchSaveInLiveSync")).toBe(settings.liveSync);
|
||||
expect(visible("batchSaveMinimumDelay")).toBe(delays);
|
||||
expect(visible("batchSaveMaximumDelay")).toBe(delays);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, false],
|
||||
[false, true],
|
||||
])("retains both batching choices across mode changes: %j", async (batchSave, batchSaveInLiveSync) => {
|
||||
const { tab, callbacks } = setup(false, batchSave, batchSaveInLiveSync);
|
||||
const switchMode = callbacks.get("syncMode")!;
|
||||
for (const mode of ["LIVESYNC", "PERIODIC", "ONEVENTS", "LIVESYNC"]) {
|
||||
await switchMode(mode);
|
||||
expect(tab.editingSettings).toMatchObject({ batchSave, batchSaveInLiveSync });
|
||||
}
|
||||
expect(tab.services.control.applySettings).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ const rangeMessages = {
|
||||
};
|
||||
|
||||
describe("Advanced setting specifications", () => {
|
||||
it("lists only the ten controls currently exposed by the Advanced page", () => {
|
||||
it("lists only the eleven controls currently exposed by the Advanced page", () => {
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => true });
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual([
|
||||
@@ -30,6 +30,7 @@ describe("Advanced setting specifications", () => {
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
@@ -40,6 +41,17 @@ describe("Advanced setting specifications", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("exposes strict content checking as an ungated Advanced toggle", () => {
|
||||
const spec = createAdvancedSettingSpecGroups({ isCouchDB: () => false })
|
||||
.find(({ heading }) => heading === "Local Database Tweak")
|
||||
?.items.find(({ key }) => key === "strictContentCheckOnStartup");
|
||||
if (!spec) throw new Error("Strict content checking is not exposed on the Advanced page");
|
||||
|
||||
expect(spec.control).toEqual({ type: "toggle" });
|
||||
expect(spec.visible).toBeUndefined();
|
||||
expect(spec.disabled).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps path identity and the four remote chunk controls CouchDB-only", () => {
|
||||
let couchDB = false;
|
||||
const groups = createAdvancedSettingSpecGroups({ isCouchDB: () => couchDB });
|
||||
|
||||
@@ -86,6 +86,7 @@ describe("settings page catalogue", () => {
|
||||
"hashCacheMaxCount",
|
||||
"chunkSplitterVersion",
|
||||
"customChunkSize",
|
||||
"strictContentCheckOnStartup",
|
||||
"pathNormalizationVersion",
|
||||
"readChunksOnline",
|
||||
"useOnlyLocalChunk",
|
||||
|
||||
Reference in new issue
Block a user