mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 02:27:07 +00:00
Refactor optional file synchronisation ownership
This commit is contained in:
@@ -54,6 +54,8 @@ const pluginDir = ".obsidian/plugins/livesync-e2e-sample";
|
||||
const pluginManifestPath = `${pluginDir}/manifest.json`;
|
||||
const pluginMainPath = `${pluginDir}/main.js`;
|
||||
const pluginStylesPath = `${pluginDir}/styles.css`;
|
||||
const pluginDataPath = `${pluginDir}/data.json`;
|
||||
const pluginSupplementaryPath = `${pluginDir}/presets.json`;
|
||||
const pluginManifestContent =
|
||||
JSON.stringify(
|
||||
{
|
||||
@@ -77,9 +79,29 @@ const pluginMainContent = [
|
||||
"",
|
||||
].join("\n");
|
||||
const pluginStylesContent = ".livesync-e2e-sample { color: #73548f; }\n";
|
||||
const pluginDataContent = JSON.stringify({ enabled: true, source: "customisation-sync-e2e" }, null, 4) + "\n";
|
||||
const pluginSupplementaryContent = JSON.stringify({ preset: "e2e", order: 1 }, null, 4) + "\n";
|
||||
const themeDir = ".obsidian/themes/livesync-e2e-theme";
|
||||
const themeManifestPath = `${themeDir}/manifest.json`;
|
||||
const themeStylesPath = `${themeDir}/theme.css`;
|
||||
const themeManifestContent =
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "LiveSync E2E Theme",
|
||||
version: "0.0.1",
|
||||
minAppVersion: "1.0.0",
|
||||
author: "Self-hosted LiveSync",
|
||||
},
|
||||
null,
|
||||
4
|
||||
) + "\n";
|
||||
const themeStylesContent = "body { --livesync-e2e-theme-colour: #3d6f54; }\n";
|
||||
const sourceDeviceName = "customisation-sync-a";
|
||||
const targetDeviceName = "customisation-sync-b";
|
||||
|
||||
type CustomisationCategory = "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA";
|
||||
type GroupedCustomisationCategory = Extract<CustomisationCategory, "PLUGIN_MAIN" | "THEME">;
|
||||
|
||||
type RunnerContext = {
|
||||
binary: string;
|
||||
cliBinary: string;
|
||||
@@ -162,6 +184,7 @@ async function startConfiguredSession(
|
||||
deviceAndVaultName: deviceName,
|
||||
usePluginSync: true,
|
||||
usePluginSyncV2: true,
|
||||
usePluginEtc: true,
|
||||
autoSweepPlugins: false,
|
||||
autoSweepPluginsPeriodic: false,
|
||||
syncInternalFiles: false,
|
||||
@@ -202,15 +225,15 @@ async function scanCustomisations(cliBinary: string, env: NodeJS.ProcessEnv): Pr
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const before=await addOn.scanInternalFiles();",
|
||||
"await addOn.scanAllConfigFiles(false);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.customisationSync;",
|
||||
"const before=await syncContext.scanInternalFiles();",
|
||||
"await syncContext.scanAllConfigFiles(false);",
|
||||
"return JSON.stringify({",
|
||||
"ok:true,",
|
||||
"enabled:core.settings.usePluginSync,",
|
||||
"useV2:core.settings.usePluginSyncV2,",
|
||||
"device:core.services.setting.getDeviceAndVaultName(),",
|
||||
"configDir:addOn.configDir,",
|
||||
"configDir:syncContext.configDir,",
|
||||
"files:before,",
|
||||
"});",
|
||||
"})()",
|
||||
@@ -226,11 +249,11 @@ async function storeCustomisationFile(cliBinary: string, env: NodeJS.ProcessEnv,
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.customisationSync;",
|
||||
"const term=core.services.setting.getDeviceAndVaultName();",
|
||||
"const stat=await core.storageAccess.statHidden(path);",
|
||||
"const category=addOn.getFileCategory(path);",
|
||||
"const result=await addOn.storeCustomizationFiles(path,term);",
|
||||
"const category=syncContext.getFileCategory(path);",
|
||||
"const result=await syncContext.storeCustomizationFiles(path,term);",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.startsWith('ix:')).map((doc)=>doc.path);",
|
||||
"const filename=path.split('/').pop();",
|
||||
@@ -248,7 +271,7 @@ async function storeCustomisationFile(cliBinary: string, env: NodeJS.ProcessEnv,
|
||||
async function deleteCustomisationSyncEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
category: CustomisationCategory,
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
@@ -260,11 +283,11 @@ async function deleteCustomisationSyncEntry(
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.customisationSync;",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`))&&!doc.deleted&&!doc._deleted)||false;",
|
||||
"if(!entry) throw new Error(`Could not find customisation sync entry to delete: ${category}/${name}`);",
|
||||
"if(!(await addOn.deleteConfigOnDatabase(entry.path))){",
|
||||
"if(!(await syncContext.deleteConfigOnDatabase(entry.path))){",
|
||||
" throw new Error(`Could not delete Customisation Sync entry: ${entry.path}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,path:entry.path});",
|
||||
@@ -277,7 +300,7 @@ async function deleteCustomisationSyncEntry(
|
||||
async function waitForCustomisationEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
category: CustomisationCategory,
|
||||
name: string,
|
||||
term?: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000)
|
||||
@@ -289,7 +312,7 @@ async function waitForCustomisationEntry(
|
||||
async function waitForCustomisationEntries(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
category: CustomisationCategory,
|
||||
name: string,
|
||||
count: number,
|
||||
term?: string,
|
||||
@@ -329,7 +352,7 @@ async function waitForCustomisationEntries(
|
||||
async function waitForCustomisationEntryAbsent(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
category: CustomisationCategory,
|
||||
name: string,
|
||||
term?: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000)
|
||||
@@ -362,7 +385,7 @@ async function waitForCustomisationEntryAbsent(
|
||||
async function applyRemoteCustomisationEntry(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "CONFIG" | "SNIPPET" | "PLUGIN_MAIN",
|
||||
category: CustomisationCategory,
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
@@ -374,16 +397,16 @@ async function applyRemoteCustomisationEntry(
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.customisationSync;",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entry=rows.map((row)=>row.doc).find((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`)))||false;",
|
||||
"if(!entry) throw new Error(`Could not find remote customisation entry: ${category}/${name}`);",
|
||||
"const display=addOn.createPluginDataFromV2(entry.path);",
|
||||
"const display=syncContext.createPluginDataFromV2(entry.path);",
|
||||
"if(!display) throw new Error(`Could not create Customisation Sync display entry: ${entry.path}`);",
|
||||
"const file=await addOn.createPluginDataExFileV2(entry.path);",
|
||||
"const file=await syncContext.createPluginDataExFileV2(entry.path);",
|
||||
"if(!file) throw new Error(`Could not load Customisation Sync file entry: ${entry.path}`);",
|
||||
"await display.setFile(file);",
|
||||
"if(!(await addOn.applyDataV2(display))){",
|
||||
"if(!(await syncContext.applyDataV2(display))){",
|
||||
" throw new Error(`Could not apply Customisation Sync entry: ${entry.path}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,path:entry.path});",
|
||||
@@ -396,7 +419,7 @@ async function applyRemoteCustomisationEntry(
|
||||
async function applyRemoteCustomisationGroup(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
category: "PLUGIN_MAIN",
|
||||
category: GroupedCustomisationCategory,
|
||||
name: string,
|
||||
term?: string
|
||||
): Promise<void> {
|
||||
@@ -408,18 +431,18 @@ async function applyRemoteCustomisationGroup(
|
||||
`const name=${JSON.stringify(name)};`,
|
||||
`const term=${JSON.stringify(term ?? "")};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('ConfigSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.customisationSync;",
|
||||
"const rows=(await core.localDatabase.allDocsRaw({include_docs:true})).rows;",
|
||||
"const entries=rows.map((row)=>row.doc).filter((doc)=>doc?.path?.includes(`/${category}/`)&&doc.path?.includes(`/${name}%`)&&(!term||doc.path?.startsWith(`ix:${term}/`)));",
|
||||
"if(entries.length===0) throw new Error(`Could not find remote customisation entries: ${category}/${name}`);",
|
||||
"const display=addOn.createPluginDataFromV2(entries[0].path);",
|
||||
"const display=syncContext.createPluginDataFromV2(entries[0].path);",
|
||||
"if(!display) throw new Error(`Could not create Customisation Sync display entry: ${entries[0].path}`);",
|
||||
"for(const entry of entries){",
|
||||
" const file=await addOn.createPluginDataExFileV2(entry.path);",
|
||||
" const file=await syncContext.createPluginDataExFileV2(entry.path);",
|
||||
" if(!file) throw new Error(`Could not load Customisation Sync file entry: ${entry.path}`);",
|
||||
" await display.setFile(file);",
|
||||
"}",
|
||||
"if(!(await addOn.applyDataV2(display))){",
|
||||
"if(!(await syncContext.applyDataV2(display))){",
|
||||
" throw new Error(`Could not apply Customisation Sync group: ${category}/${name}`);",
|
||||
"}",
|
||||
"return JSON.stringify({ok:true,count:entries.length});",
|
||||
@@ -445,6 +468,7 @@ async function main(): Promise<void> {
|
||||
const snippetName = snippetPathParts[snippetPathParts.length - 1] ?? snippetPath;
|
||||
const configName = configPath.split("/").pop() ?? configPath;
|
||||
const pluginName = pluginDir.split("/").pop() ?? pluginDir;
|
||||
const themeName = themeDir.split("/").pop() ?? themeDir;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
@@ -460,6 +484,10 @@ async function main(): Promise<void> {
|
||||
await writeVaultFile(vaultA.path, pluginManifestPath, pluginManifestContent);
|
||||
await writeVaultFile(vaultA.path, pluginMainPath, pluginMainContent);
|
||||
await writeVaultFile(vaultA.path, pluginStylesPath, pluginStylesContent);
|
||||
await writeVaultFile(vaultA.path, pluginDataPath, pluginDataContent);
|
||||
await writeVaultFile(vaultA.path, pluginSupplementaryPath, pluginSupplementaryContent);
|
||||
await writeVaultFile(vaultA.path, themeManifestPath, themeManifestContent);
|
||||
await writeVaultFile(vaultA.path, themeStylesPath, themeStylesContent);
|
||||
|
||||
let session = await startConfiguredSession(context, vaultA, sourceDeviceName);
|
||||
const scanResult = await scanCustomisations(context.cliBinary, session.cliEnv);
|
||||
@@ -469,7 +497,11 @@ async function main(): Promise<void> {
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginManifestPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginMainPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginStylesPath);
|
||||
const entry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginDataPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, pluginSupplementaryPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, themeManifestPath);
|
||||
await storeCustomisationFile(context.cliBinary, session.cliEnv, themeStylesPath);
|
||||
const snippetEntry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName);
|
||||
const configEntry = await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "CONFIG", configName);
|
||||
const pluginEntries = await waitForCustomisationEntries(
|
||||
context.cliBinary,
|
||||
@@ -478,10 +510,36 @@ async function main(): Promise<void> {
|
||||
pluginName,
|
||||
3
|
||||
);
|
||||
const pluginDataEntry = await waitForCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_DATA",
|
||||
pluginName
|
||||
);
|
||||
const pluginSupplementaryEntry = await waitForCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_ETC",
|
||||
pluginName
|
||||
);
|
||||
const themeEntries = await waitForCustomisationEntries(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"THEME",
|
||||
themeName,
|
||||
2
|
||||
);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => {
|
||||
const ids = new Set(docs.map((doc) => doc._id));
|
||||
const entries = [entry, configEntry, ...pluginEntries];
|
||||
const entries = [
|
||||
snippetEntry,
|
||||
configEntry,
|
||||
...pluginEntries,
|
||||
pluginDataEntry,
|
||||
pluginSupplementaryEntry,
|
||||
...themeEntries,
|
||||
];
|
||||
return entries.every(
|
||||
(target) => ids.has(target.id) && target.children.every((childId) => ids.has(childId))
|
||||
);
|
||||
@@ -491,11 +549,33 @@ async function main(): Promise<void> {
|
||||
session = await startConfiguredSession(context, vaultB, targetDeviceName);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "SNIPPET", snippetName, sourceDeviceName);
|
||||
assertEqual(
|
||||
await pathExists(vaultB.path, snippetPath),
|
||||
false,
|
||||
"Customisation Sync snippet was reflected before explicit application."
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "CONFIG", configName, sourceDeviceName);
|
||||
await waitForCustomisationEntries(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_MAIN",
|
||||
pluginName,
|
||||
3,
|
||||
sourceDeviceName
|
||||
);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "PLUGIN_DATA", pluginName, sourceDeviceName);
|
||||
await waitForCustomisationEntry(context.cliBinary, session.cliEnv, "PLUGIN_ETC", pluginName, sourceDeviceName);
|
||||
await waitForCustomisationEntries(context.cliBinary, session.cliEnv, "THEME", themeName, 2, sourceDeviceName);
|
||||
const unappliedPaths: Array<[path: string, description: string]> = [
|
||||
[snippetPath, "snippet"],
|
||||
[configPath, "configuration file"],
|
||||
[pluginManifestPath, "plug-in main file"],
|
||||
[pluginDataPath, "plug-in data file"],
|
||||
[pluginSupplementaryPath, "plug-in supplementary file"],
|
||||
[themeManifestPath, "theme file"],
|
||||
];
|
||||
for (const [path, description] of unappliedPaths) {
|
||||
assertEqual(
|
||||
await pathExists(vaultB.path, path),
|
||||
false,
|
||||
`Customisation Sync ${description} was reflected before explicit application.`
|
||||
);
|
||||
}
|
||||
await applyRemoteCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
@@ -528,6 +608,41 @@ async function main(): Promise<void> {
|
||||
pluginStylesPath,
|
||||
(content) => content === pluginStylesContent
|
||||
);
|
||||
await applyRemoteCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_DATA",
|
||||
pluginName,
|
||||
sourceDeviceName
|
||||
);
|
||||
const appliedPluginData = await waitForPathContent(
|
||||
vaultB.path,
|
||||
pluginDataPath,
|
||||
(content) => content === pluginDataContent
|
||||
);
|
||||
await applyRemoteCustomisationEntry(
|
||||
context.cliBinary,
|
||||
session.cliEnv,
|
||||
"PLUGIN_ETC",
|
||||
pluginName,
|
||||
sourceDeviceName
|
||||
);
|
||||
const appliedPluginSupplementary = await waitForPathContent(
|
||||
vaultB.path,
|
||||
pluginSupplementaryPath,
|
||||
(content) => content === pluginSupplementaryContent
|
||||
);
|
||||
await applyRemoteCustomisationGroup(context.cliBinary, session.cliEnv, "THEME", themeName, sourceDeviceName);
|
||||
const appliedThemeManifest = await waitForPathContent(
|
||||
vaultB.path,
|
||||
themeManifestPath,
|
||||
(content) => content === themeManifestContent
|
||||
);
|
||||
const appliedThemeStyles = await waitForPathContent(
|
||||
vaultB.path,
|
||||
themeStylesPath,
|
||||
(content) => content === themeStylesContent
|
||||
);
|
||||
await session.app.stop();
|
||||
|
||||
assertEqual(applied, snippetContent, "Customisation Sync snippet content did not match after application.");
|
||||
@@ -539,6 +654,14 @@ async function main(): Promise<void> {
|
||||
);
|
||||
assertEqual(appliedPluginMain, pluginMainContent, "Customisation Sync plug-in main file did not match.");
|
||||
assertEqual(appliedPluginStyles, pluginStylesContent, "Customisation Sync plug-in stylesheet did not match.");
|
||||
assertEqual(appliedPluginData, pluginDataContent, "Customisation Sync plug-in data did not match.");
|
||||
assertEqual(
|
||||
appliedPluginSupplementary,
|
||||
pluginSupplementaryContent,
|
||||
"Customisation Sync plug-in supplementary file did not match."
|
||||
);
|
||||
assertEqual(appliedThemeManifest, themeManifestContent, "Customisation Sync theme manifest did not match.");
|
||||
assertEqual(appliedThemeStyles, themeStylesContent, "Customisation Sync theme stylesheet did not match.");
|
||||
|
||||
await writeVaultFile(vaultA.path, snippetPath, snippetUpdatedContent);
|
||||
session = await startConfiguredSession(context, vaultA, sourceDeviceName);
|
||||
@@ -589,7 +712,7 @@ async function main(): Promise<void> {
|
||||
await session.app.stop();
|
||||
|
||||
console.log(
|
||||
`Customisation Sync applied snippet, config, and plug-in fixtures, then propagated snippet update and sync-data deletion.`
|
||||
`Customisation Sync applied configuration, theme, snippet, and plug-in main, data, and supplementary fixtures, then propagated snippet update and sync-data deletion.`
|
||||
);
|
||||
} finally {
|
||||
await vaultA.dispose();
|
||||
|
||||
@@ -745,14 +745,20 @@ async function verifyCompatibleAlignmentSettingDefault(): Promise<void> {
|
||||
}
|
||||
|
||||
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||
const liveSyncSettings = await settingsNavigator.openPage("Advanced");
|
||||
const settingItem = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: settingsNavigator.page.getByText("Auto-accept compatible tweak mismatches", { exact: true }),
|
||||
});
|
||||
await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const toggle = settingItem.locator(".checkbox-container");
|
||||
if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) {
|
||||
throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined.");
|
||||
try {
|
||||
const liveSyncSettings = await settingsNavigator.openPage("Advanced");
|
||||
const settingItem = liveSyncSettings.locator(".setting-item").filter({
|
||||
has: settingsNavigator.page.getByText("Auto-accept compatible tweak mismatches", { exact: true }),
|
||||
});
|
||||
await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const toggle = settingItem.locator(".checkbox-container");
|
||||
if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) {
|
||||
throw new Error(
|
||||
"The automatic compatible-setting policy was displayed as disabled while still undefined."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await settingsNavigator.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -873,6 +879,43 @@ async function executeRegisteredCommand(commandId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCustomisationSyncDialogue(): Promise<string> {
|
||||
const commandId = "obsidian-livesync:livesync-plugin-dialog-ex";
|
||||
await executeRegisteredCommand(commandId);
|
||||
const screenshotPath = await captureObsidianDialogue(
|
||||
obsidianRemoteDebuggingPort(),
|
||||
"customisation-sync-dialogue.png",
|
||||
async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Customization Sync (Beta3)" }),
|
||||
});
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
for (const action of ["Scan changes", "Sync once", "Refresh", "Apply All Selected"]) {
|
||||
await modal.getByRole("button", { name: action, exact: true }).waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
for (let openCount = 0; openCount < 2; openCount++) {
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Customization Sync (Beta3)" }),
|
||||
});
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await page.keyboard.press("Escape");
|
||||
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
});
|
||||
if (openCount === 0) {
|
||||
await executeRegisteredCommand(commandId);
|
||||
}
|
||||
}
|
||||
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function verifyLogAndReportSurfaces(): Promise<{ log: string; report: string }> {
|
||||
await executeRegisteredCommand("obsidian-livesync:view-log");
|
||||
const logScreenshot = await captureObsidianElement(
|
||||
@@ -1198,6 +1241,8 @@ async function main(): Promise<void> {
|
||||
syncAfterMerge: false,
|
||||
periodicReplication: false,
|
||||
useAdvancedMode: true,
|
||||
usePluginSync: true,
|
||||
deviceAndVaultName: "dialogue-mounts",
|
||||
}
|
||||
),
|
||||
});
|
||||
@@ -1233,6 +1278,11 @@ async function main(): Promise<void> {
|
||||
`Compatibility review actions were stacked vertically, and the remote-size startup notice opened an untimed review dialogue successfully. Screenshots: ${remoteSizeScreenshots.compatibilityReview}, ${remoteSizeScreenshots.notice}, ${remoteSizeScreenshots.dialogue}`
|
||||
);
|
||||
|
||||
const customisationSyncScreenshot = await verifyCustomisationSyncDialogue();
|
||||
console.log(
|
||||
`The Customisation Sync command mounted, closed, and remounted its focused-view dialogue successfully. Screenshot: ${customisationSyncScreenshot}`
|
||||
);
|
||||
|
||||
const remoteScreenshot = await verifyRemoteSelectionDialogue("desktop");
|
||||
console.log(`Remote selection dialogue mounted and closed successfully. Screenshot: ${remoteScreenshot}`);
|
||||
const couchDBScreenshot = await verifyCouchDBSettingsDialogue("desktop");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { MODE_AUTOMATIC, MODE_PAUSED } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
assertLocatorHasMinimumTouchTarget,
|
||||
assertLocatorWithinSafeArea,
|
||||
@@ -60,6 +61,9 @@ const manualMergeJsonPath = ".obsidian/livesync-e2e-manual-merge.json";
|
||||
const targetPath = ".obsidian/livesync-targeted/only-a.json";
|
||||
const hiddenFileCliTimeoutMs = Number(process.env.E2E_OBSIDIAN_HIDDEN_FILE_CLI_TIMEOUT_MS ?? 90000);
|
||||
const hiddenFileInitialisationStateKey = "__livesyncE2EHiddenFileInitialisation";
|
||||
const mixedSelectivePath = ".obsidian/snippets/livesync-mixed-selective.css";
|
||||
const mixedAutomaticPath = ".obsidian/snippets/livesync-mixed-automatic.css";
|
||||
const mixedPausedPath = ".obsidian/snippets/livesync-mixed-paused.css";
|
||||
|
||||
type RunnerContext = {
|
||||
binary: string;
|
||||
@@ -144,8 +148,8 @@ async function scanHiddenStorage(cliBinary: string, env: NodeJS.ProcessEnv): Pro
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"await addOn.scanAllStorageChanges(true);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"await syncContext.scanAllStorageChanges(true);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -159,8 +163,8 @@ async function scanHiddenDatabase(cliBinary: string, env: NodeJS.ProcessEnv): Pr
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"await addOn.scanAllDatabaseChanges(true);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"await syncContext.scanAllDatabaseChanges(true);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -175,9 +179,9 @@ async function resolveHiddenConflicts(cliBinary: string, env: NodeJS.ProcessEnv)
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"await addOn.resolveConflictOnInternalFiles();",
|
||||
"await addOn.scanAllDatabaseChanges(true);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"await syncContext.resolveConflictOnInternalFiles();",
|
||||
"await syncContext.scanAllDatabaseChanges(true);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -194,7 +198,7 @@ async function autoMergeHiddenJsonConflict(cliBinary: string, env: NodeJS.Proces
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
"const prefixedPath=`i:${path}`;",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"let doc=false;",
|
||||
"for await (const entry of core.localDatabase.findEntries('i:','i;',{conflicts:true})){",
|
||||
" if(entry.path===prefixedPath){ doc=entry; break; }",
|
||||
@@ -214,13 +218,13 @@ async function autoMergeHiddenJsonConflict(cliBinary: string, env: NodeJS.Proces
|
||||
"if(!result){",
|
||||
" throw new Error(`Hidden JSON conflict was not auto-mergeable: ${path}; base=${commonBase}; current=${doc._rev}; conflict=${conflictedRev}`);",
|
||||
"}",
|
||||
"await addOn.ensureDir(path);",
|
||||
"const stat=await addOn.writeFile(path,result);",
|
||||
"await syncContext.ensureDir(path);",
|
||||
"const stat=await syncContext.writeFile(path,result);",
|
||||
"if(!stat) throw new Error(`Could not write merged hidden file: ${path}`);",
|
||||
"await addOn.storeInternalFileToDatabase({path,mtime:stat.mtime,ctime:stat.ctime,size:stat.size},true);",
|
||||
"await syncContext.storeInternalFileToDatabase({path,mtime:stat.mtime,ctime:stat.ctime,size:stat.size},true);",
|
||||
"await core.localDatabase.removeRevision(doc._id,conflictedRev);",
|
||||
"await addOn.extractInternalFileFromDatabase(path);",
|
||||
"await addOn.scanAllDatabaseChanges(true);",
|
||||
"await syncContext.extractInternalFileFromDatabase(path);",
|
||||
"await syncContext.scanAllDatabaseChanges(true);",
|
||||
"return JSON.stringify({ok:true,merged:JSON.parse(result)});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -236,7 +240,7 @@ async function openHiddenJsonResolveModal(cliBinary: string, env: NodeJS.Process
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
"const prefixedPath=`i:${path}`;",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"let doc=false;",
|
||||
"for await (const entry of core.localDatabase.findEntries('i:','i;',{conflicts:true})){",
|
||||
" if(entry.path===prefixedPath){ doc=entry; break; }",
|
||||
@@ -246,7 +250,7 @@ async function openHiddenJsonResolveModal(cliBinary: string, env: NodeJS.Process
|
||||
"const docA=await core.localDatabase.getDBEntry(prefixedPath,{rev:doc._rev});",
|
||||
"const docB=await core.localDatabase.getDBEntry(prefixedPath,{rev:conflicts[0]});",
|
||||
"if(docA===false||docB===false) throw new Error(`Could not load conflicted hidden JSON entries: ${path}`);",
|
||||
"void addOn.showJSONMergeDialogAndMerge(docA,docB);",
|
||||
"void syncContext.showJSONMergeDialogAndMerge(docA,docB);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -267,10 +271,10 @@ async function storeHiddenFileAsConflict(
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const baseRev=${JSON.stringify(baseRev)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"const fileInfo=await addOn.loadFileWithInfo(path);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"const fileInfo=await syncContext.loadFileWithInfo(path);",
|
||||
"if(fileInfo.deleted) throw new Error(`Hidden file was unexpectedly deleted: ${path}`);",
|
||||
"const baseData=await addOn.__loadBaseSaveData(path,true);",
|
||||
"const baseData=await syncContext.__loadBaseSaveData(path,true);",
|
||||
"if(baseData===false) throw new Error(`Could not load base save data: ${path}`);",
|
||||
"const saveData={",
|
||||
" ...baseData,",
|
||||
@@ -509,6 +513,91 @@ async function runTargetMismatch(
|
||||
console.log("Hidden target mismatch respected per-device target patterns, then applied after enabling the target.");
|
||||
}
|
||||
|
||||
async function runMixedOwnership(context: RunnerContext, vault: TemporaryVault): Promise<void> {
|
||||
const content = ".livesync-mixed-owner { color: #245a70; }\n";
|
||||
await writeVaultFile(vault.path, mixedSelectivePath, content);
|
||||
await writeVaultFile(vault.path, mixedAutomaticPath, content);
|
||||
await writeVaultFile(vault.path, mixedPausedPath, content);
|
||||
|
||||
const session = await startConfiguredSession(context, vault, {
|
||||
deviceAndVaultName: "mixed-ownership",
|
||||
usePluginSync: true,
|
||||
usePluginSyncV2: true,
|
||||
usePluginEtc: true,
|
||||
pluginSyncExtendedSetting: {
|
||||
"SNIPPET/livesync-mixed-automatic.css": {
|
||||
key: "SNIPPET/livesync-mixed-automatic.css",
|
||||
mode: MODE_AUTOMATIC,
|
||||
files: ["snippets/livesync-mixed-automatic.css"],
|
||||
},
|
||||
"SNIPPET/livesync-mixed-paused.css": {
|
||||
key: "SNIPPET/livesync-mixed-paused.css",
|
||||
mode: MODE_PAUSED,
|
||||
files: ["snippets/livesync-mixed-paused.css"],
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await evalObsidianJson<{ hiddenPaths: string[]; customisationPaths: string[] }>(
|
||||
context.cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
"const plugin=app.plugins.plugins['obsidian-livesync'];",
|
||||
"const core=plugin.core;",
|
||||
"const customisation=plugin.optionalFileSync.testing.customisationSync;",
|
||||
"const hidden=plugin.optionalFileSync.testing.hiddenFileSync;",
|
||||
"core.services.setting.setDeviceAndVaultName('mixed-ownership');",
|
||||
"await customisation.scanAllConfigFiles(false);",
|
||||
"await hidden.scanAllStorageChanges(false,false,true,true);",
|
||||
"const customisationPaths=[];",
|
||||
"for await(const entry of core.localDatabase.findEntries('ix:','ix;')){customisationPaths.push(entry.path);}",
|
||||
"const hiddenPaths=[];",
|
||||
"for await(const entry of core.localDatabase.findEntries('i:','i;')){hiddenPaths.push(entry.path);}",
|
||||
"return JSON.stringify({customisationPaths,hiddenPaths});",
|
||||
"})()",
|
||||
].join(""),
|
||||
session.cliEnv,
|
||||
hiddenFileCliTimeoutMs
|
||||
);
|
||||
|
||||
const selectiveDocument =
|
||||
"ix:mixed-ownership/SNIPPET/livesync-mixed-selective.css%livesync-mixed-selective.css";
|
||||
const automaticDocument = `i:${mixedAutomaticPath}`;
|
||||
assertEqual(
|
||||
result.customisationPaths.includes(selectiveDocument),
|
||||
true,
|
||||
"Selective mode did not create its Customisation Sync document."
|
||||
);
|
||||
assertEqual(
|
||||
result.hiddenPaths.includes(`i:${mixedSelectivePath}`),
|
||||
false,
|
||||
"Selective mode also created a Hidden File Sync document."
|
||||
);
|
||||
assertEqual(
|
||||
result.hiddenPaths.includes(automaticDocument),
|
||||
true,
|
||||
"Automatic mode did not create its Hidden File Sync document."
|
||||
);
|
||||
assertEqual(
|
||||
result.customisationPaths.some((path) => path.includes("livesync-mixed-automatic.css")),
|
||||
false,
|
||||
"Automatic mode also created a Customisation Sync document."
|
||||
);
|
||||
assertEqual(
|
||||
result.hiddenPaths.includes(`i:${mixedPausedPath}`) ||
|
||||
result.customisationPaths.some((path) => path.includes("livesync-mixed-paused.css")),
|
||||
false,
|
||||
"Ignore mode created an optional-file document."
|
||||
);
|
||||
} finally {
|
||||
await session.app.stop();
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Mixed optional-file ownership stored Selective, Automatic, and Ignore paths in at most one namespace."
|
||||
);
|
||||
}
|
||||
|
||||
async function setHiddenFileNoticeFixtures(port: number, itemIds: string[], includeRestart: boolean): Promise<void> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate(
|
||||
@@ -516,7 +605,7 @@ async function setHiddenFileNoticeFixtures(port: number, itemIds: string[], incl
|
||||
const obsidianApp = (globalThis as typeof globalThis & { app: any }).app;
|
||||
const plugin = obsidianApp.plugins.plugins["obsidian-livesync"];
|
||||
const core = plugin.core;
|
||||
const addOn = core.getAddOn("HiddenFileSync");
|
||||
const syncContext = plugin.optionalFileSync.testing.hiddenFileSync;
|
||||
for (const id of ["alpha", "beta", "gamma"]) {
|
||||
const pluginId = `livesync-e2e-${id}`;
|
||||
obsidianApp.plugins.manifests[pluginId] = {
|
||||
@@ -531,14 +620,14 @@ async function setHiddenFileNoticeFixtures(port: number, itemIds: string[], incl
|
||||
};
|
||||
obsidianApp.plugins.enabledPlugins.add(pluginId);
|
||||
}
|
||||
addOn.queuedNotificationFiles.clear();
|
||||
syncContext.queuedNotificationFiles.clear();
|
||||
for (const id of nextItemIds) {
|
||||
addOn.queuedNotificationFiles.add(`.obsidian/plugins/livesync-e2e-${id}`);
|
||||
syncContext.queuedNotificationFiles.add(`.obsidian/plugins/livesync-e2e-${id}`);
|
||||
}
|
||||
if (nextIncludeRestart) {
|
||||
addOn.queuedNotificationFiles.add(core.services.API.getSystemConfigDir());
|
||||
syncContext.queuedNotificationFiles.add(core.services.API.getSystemConfigDir());
|
||||
}
|
||||
addOn.notifyConfigChange();
|
||||
syncContext.notifyConfigChange();
|
||||
},
|
||||
{ nextItemIds: itemIds, nextIncludeRestart: includeRestart }
|
||||
);
|
||||
@@ -571,11 +660,14 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while ((await page.locator(".notice:visible").count()) > 0 && Date.now() < deadline) {
|
||||
await page.locator(".notice:visible").first().click({
|
||||
force: true,
|
||||
position: { x: 2, y: 2 },
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
await page
|
||||
.locator(".notice:visible")
|
||||
.first()
|
||||
.click({
|
||||
force: true,
|
||||
position: { x: 2, y: 2 },
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
}
|
||||
assertEqual(
|
||||
await page.locator(".notice:visible").count(),
|
||||
@@ -588,10 +680,10 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
const obsidianApp = (globalThis as typeof globalThis & { app: any }).app;
|
||||
const plugin = obsidianApp.plugins.plugins["obsidian-livesync"];
|
||||
const core = plugin.core;
|
||||
const addOn = core.getAddOn("HiddenFileSync");
|
||||
const syncContext = plugin.optionalFileSync.testing.hiddenFileSync;
|
||||
const setting = core.services.setting;
|
||||
const originalApplyPartial = setting.applyPartial;
|
||||
const originalRebuildMerging = addOn.rebuildMerging;
|
||||
const originalRebuildMerging = syncContext.rebuildMerging;
|
||||
const state = {
|
||||
done: false,
|
||||
reachedPreparation: false,
|
||||
@@ -639,12 +731,12 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
return await originalApplyPartial.apply(setting, args);
|
||||
};
|
||||
|
||||
addOn.rebuildMerging = async (...args: unknown[]) => {
|
||||
syncContext.rebuildMerging = async (...args: unknown[]) => {
|
||||
state.reachedInitialisation = true;
|
||||
await new Promise<void>((resolve) => {
|
||||
state.releaseInitialisation = resolve;
|
||||
});
|
||||
return await originalRebuildMerging.apply(addOn, args);
|
||||
return await originalRebuildMerging.apply(syncContext, args);
|
||||
};
|
||||
|
||||
void core.services.setting
|
||||
@@ -660,7 +752,7 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
)
|
||||
.finally(() => {
|
||||
setting.applyPartial = originalApplyPartial;
|
||||
addOn.rebuildMerging = originalRebuildMerging;
|
||||
syncContext.rebuildMerging = originalRebuildMerging;
|
||||
const notices = Array.from(document.querySelectorAll<HTMLElement>(".notice"));
|
||||
const progressNotices = notices.filter((notice) => notice.textContent?.includes("[⚙"));
|
||||
state.sawStandaloneGatheringNotice ||= notices.some((notice) =>
|
||||
@@ -707,17 +799,15 @@ async function runInitialisationNoticeGrouping(context: RunnerContext, vault: Te
|
||||
|
||||
const result = await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate((stateKey) => {
|
||||
const state = (globalThis as unknown as Record<
|
||||
string,
|
||||
{ releasePreparation?: () => void } | undefined
|
||||
>)[stateKey];
|
||||
const state = (
|
||||
globalThis as unknown as Record<string, { releasePreparation?: () => void } | undefined>
|
||||
)[stateKey];
|
||||
state?.releasePreparation?.();
|
||||
}, hiddenFileInitialisationStateKey);
|
||||
await page.waitForFunction(
|
||||
(stateKey) =>
|
||||
(globalThis as unknown as Record<string, { reachedInitialisation?: boolean } | undefined>)[
|
||||
stateKey
|
||||
]?.reachedInitialisation === true,
|
||||
(globalThis as unknown as Record<string, { reachedInitialisation?: boolean } | undefined>)[stateKey]
|
||||
?.reachedInitialisation === true,
|
||||
hiddenFileInitialisationStateKey,
|
||||
{ timeout: timeoutMs }
|
||||
);
|
||||
@@ -872,6 +962,7 @@ async function main(): Promise<void> {
|
||||
await runJsonConflictRoundTrip(context, vaultA, vaultB);
|
||||
await runJsonManualConflictResolution(context, vaultB);
|
||||
await runTargetMismatch(context, vaultA, vaultB);
|
||||
await runMixedOwnership(context, vaultB);
|
||||
await runInitialisationNoticeGrouping(context, vaultB);
|
||||
await runConfigurationNoticeGrouping(context, vaultB);
|
||||
} finally {
|
||||
|
||||
@@ -589,8 +589,8 @@ async function scanHiddenStorage(cliBinary: string, environment: NodeJS.ProcessE
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"await addOn.scanAllStorageChanges(true);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"await syncContext.scanAllStorageChanges(true);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
@@ -605,8 +605,8 @@ async function scanHiddenDatabase(cliBinary: string, environment: NodeJS.Process
|
||||
[
|
||||
"(async()=>{",
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const addOn=core.getAddOn('HiddenFileSync');",
|
||||
"await addOn.scanAllDatabaseChanges(true);",
|
||||
"const syncContext=app.plugins.plugins['obsidian-livesync'].optionalFileSync.testing.hiddenFileSync;",
|
||||
"await syncContext.scanAllDatabaseChanges(true);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
|
||||
Reference in New Issue
Block a user