mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-04 17:57:05 +00:00
Test conflict dialogue concurrency and unload lifecycle
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
waitForLocalDatabaseEntry,
|
||||
} from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts";
|
||||
import { captureObsidianElement, captureObsidianPage, withObsidianPage } from "../runner/ui.ts";
|
||||
import { createTemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
const path = "conflict-dialog-policy.md";
|
||||
@@ -14,6 +14,12 @@ const baseContent = "Conflict dialogue policy\n\nShared base.\n";
|
||||
const leftContent = "Conflict dialogue policy\n\nChanged on the left.\n";
|
||||
const rightContent = "Conflict dialogue policy\n\nChanged on the right.\n";
|
||||
const thirdContent = "Conflict dialogue policy\n\nChanged on the third branch.\n";
|
||||
const repeatedPath = "conflict-dialog-repeated.md";
|
||||
const activePath = "conflict-dialog-active.md";
|
||||
const waitingPath = "conflict-dialog-waiting.md";
|
||||
const externallyResolvedWaitingPath = "conflict-dialog-resolved-while-waiting.md";
|
||||
const unloadActivePath = "conflict-dialog-unload-active.md";
|
||||
const unloadWaitingPath = "conflict-dialog-unload-waiting.md";
|
||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_CONFLICT_DIALOG_TIMEOUT_MS ?? 10000);
|
||||
|
||||
type ConflictFixture = {
|
||||
@@ -24,16 +30,35 @@ type ConflictFixture = {
|
||||
|
||||
type ObsidianTestApp = {
|
||||
commands?: { executeCommandById(commandId: string): boolean };
|
||||
plugins?: {
|
||||
disablePlugin(pluginId: string): Promise<void>;
|
||||
enablePlugin(pluginId: string): Promise<void>;
|
||||
plugins?: Record<
|
||||
string,
|
||||
| {
|
||||
core?: {
|
||||
services?: {
|
||||
conflict?: { ensureAllProcessed(): Promise<boolean> };
|
||||
};
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
|
||||
type ObsidianTestGlobal = typeof globalThis & {
|
||||
app?: ObsidianTestApp;
|
||||
__livesyncConflictChecksCompleted?: boolean;
|
||||
__livesyncWaitingConflictCompleted?: boolean;
|
||||
};
|
||||
|
||||
async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): Promise<void> {
|
||||
async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv, targetPath = path): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const content=${JSON.stringify(baseContent)};`,
|
||||
"let file=app.vault.getAbstractFileByPath(path);",
|
||||
"if(!file) file=await app.vault.create(path,content);",
|
||||
@@ -49,13 +74,14 @@ async function createManualConflict(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
baseRev: string,
|
||||
contents: readonly string[]
|
||||
contents: readonly string[],
|
||||
targetPath = path
|
||||
): Promise<ConflictFixture> {
|
||||
return await evalObsidianJson<ConflictFixture>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const baseRev=${JSON.stringify(baseRev)};`,
|
||||
`const contents=${JSON.stringify(contents)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
@@ -81,12 +107,16 @@ async function createManualConflict(
|
||||
);
|
||||
}
|
||||
|
||||
async function readConflictFixture(cliBinary: string, env: NodeJS.ProcessEnv): Promise<ConflictFixture> {
|
||||
async function readConflictFixture(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
targetPath = path
|
||||
): Promise<ConflictFixture> {
|
||||
return await evalObsidianJson<ConflictFixture>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"const meta=await core.localDatabase.getDBEntryMeta(path,{conflicts:true,revs:true},true);",
|
||||
"if(!meta?._rev){",
|
||||
@@ -106,13 +136,14 @@ async function readConflictFixture(cliBinary: string, env: NodeJS.ProcessEnv): P
|
||||
async function waitForConflictCount(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
expectedConflictCount: number
|
||||
expectedConflictCount: number,
|
||||
targetPath = path
|
||||
): Promise<ConflictFixture> {
|
||||
const deadline = Date.now() + uiTimeoutMs;
|
||||
let fixture = await readConflictFixture(cliBinary, env);
|
||||
let fixture = await readConflictFixture(cliBinary, env, targetPath);
|
||||
while (fixture.conflicts.length !== expectedConflictCount && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
fixture = await readConflictFixture(cliBinary, env);
|
||||
fixture = await readConflictFixture(cliBinary, env, targetPath);
|
||||
}
|
||||
if (fixture.conflicts.length !== expectedConflictCount) {
|
||||
throw new Error(
|
||||
@@ -122,12 +153,17 @@ async function waitForConflictCount(
|
||||
return fixture;
|
||||
}
|
||||
|
||||
async function requestConflictCheck(cliBinary: string, env: NodeJS.ProcessEnv, waitForCompletion = false) {
|
||||
async function requestConflictCheck(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
waitForCompletion = false,
|
||||
targetPath = path
|
||||
) {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const waitForCompletion=${JSON.stringify(waitForCompletion)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"await core.services.conflict.queueCheckFor(path);",
|
||||
@@ -159,13 +195,14 @@ async function applyReplicatedConflictResolution(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
revisionToDelete: string,
|
||||
expectedConflictCount = 0
|
||||
expectedConflictCount = 0,
|
||||
targetPath = path
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const revisionToDelete=${JSON.stringify(revisionToDelete)};`,
|
||||
`const expectedConflictCount=${JSON.stringify(expectedConflictCount)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
@@ -191,10 +228,135 @@ async function applyReplicatedConflictResolution(
|
||||
);
|
||||
}
|
||||
|
||||
function conflictDialogue(page: Parameters<Parameters<typeof withObsidianPage>[1]>[0]) {
|
||||
return page.locator(".modal-container").filter({
|
||||
function conflictDialogue(page: Parameters<Parameters<typeof withObsidianPage>[1]>[0], targetPath?: string) {
|
||||
const dialogues = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Conflicting changes" }),
|
||||
});
|
||||
return targetPath === undefined ? dialogues : dialogues.filter({ hasText: targetPath });
|
||||
}
|
||||
|
||||
async function createTwoVersionConflict(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
targetPath: string
|
||||
): Promise<ConflictFixture> {
|
||||
await createAndOpenBaseFile(cliBinary, env, targetPath);
|
||||
const base = await waitForLocalDatabaseEntry(cliBinary, env, targetPath);
|
||||
const fixture = await createManualConflict(cliBinary, env, base.rev, [leftContent, rightContent], targetPath);
|
||||
if (fixture.conflicts.length !== 1) {
|
||||
throw new Error(`Expected exactly two live leaves for ${targetPath}: ${JSON.stringify(fixture)}`);
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
async function setShowMergeDialogOnlyOnActive(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
enabled: boolean
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(()=>{",
|
||||
`const enabled=${JSON.stringify(enabled)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"core.settings.showMergeDialogOnlyOnActive=enabled;",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function requestRepeatedConflictChecks(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
targetPath: string,
|
||||
count: number
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const count=${JSON.stringify(count)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"await Promise.all(Array.from({length:count},()=>core.services.conflict.queueCheckFor(path)));",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function requestInteractiveConflictResolution(
|
||||
cliBinary: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
targetPath: string,
|
||||
fixture: ConflictFixture,
|
||||
trackCompletion = false
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(()=>{",
|
||||
`const path=${JSON.stringify(targetPath)};`,
|
||||
`const currentRev=${JSON.stringify(fixture.currentRev)};`,
|
||||
`const conflictRev=${JSON.stringify(fixture.conflicts[0])};`,
|
||||
`const trackCompletion=${JSON.stringify(trackCompletion)};`,
|
||||
"const core=app.plugins.plugins['obsidian-livesync'].core;",
|
||||
"if(trackCompletion) globalThis.__livesyncWaitingConflictCompleted=false;",
|
||||
"const pending=core.services.conflict.resolveByUserInteraction(path,{",
|
||||
"left:{rev:currentRev,data:'Current branch',ctime:1,mtime:2},",
|
||||
"right:{rev:conflictRev,data:'Conflict branch',ctime:1,mtime:3},",
|
||||
"diff:[[0,'Current and conflict branches']],",
|
||||
"});",
|
||||
"if(trackCompletion) void pending.then(()=>{globalThis.__livesyncWaitingConflictCompleted=true;});",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
env
|
||||
);
|
||||
}
|
||||
|
||||
async function disableLiveSyncAndWaitForConflictChecks(port: number): Promise<void> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate(() => {
|
||||
const host = globalThis as ObsidianTestGlobal;
|
||||
const conflict = host.app?.plugins?.plugins?.["obsidian-livesync"]?.core?.services?.conflict;
|
||||
if (conflict === undefined) throw new Error("LiveSync conflict service is unavailable before unload");
|
||||
host.__livesyncConflictChecksCompleted = false;
|
||||
void conflict.ensureAllProcessed().then(() => {
|
||||
host.__livesyncConflictChecksCompleted = true;
|
||||
});
|
||||
});
|
||||
await page.evaluate(async () => {
|
||||
const plugins = (globalThis as ObsidianTestGlobal).app?.plugins;
|
||||
if (plugins === undefined) throw new Error("Obsidian plug-in manager is unavailable");
|
||||
await plugins.disablePlugin("obsidian-livesync");
|
||||
});
|
||||
await conflictDialogue(page).waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const host = globalThis as ObsidianTestGlobal;
|
||||
return (
|
||||
host.__livesyncConflictChecksCompleted === true && host.__livesyncWaitingConflictCompleted === true
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function enableLiveSync(port: number): Promise<void> {
|
||||
await withObsidianPage(port, async (page) => {
|
||||
await page.evaluate(async () => {
|
||||
const plugins = (globalThis as ObsidianTestGlobal).app?.plugins;
|
||||
if (plugins === undefined) throw new Error("Obsidian plug-in manager is unavailable");
|
||||
await plugins.enablePlugin("obsidian-livesync");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
@@ -415,11 +577,186 @@ async function main(): Promise<void> {
|
||||
.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
// Keep concurrent manual comparisons independent from the active-file
|
||||
// gate so that the dialogue serialisation policy is exercised directly.
|
||||
await setShowMergeDialogOnlyOnActive(cliBinary, session.cliEnv, false);
|
||||
|
||||
await createTwoVersionConflict(cliBinary, session.cliEnv, repeatedPath);
|
||||
await requestConflictCheck(cliBinary, session.cliEnv, false, repeatedPath);
|
||||
const repeatedSession = session;
|
||||
await withObsidianPage(repeatedSession.remoteDebuggingPort, async (page) => {
|
||||
const modal = conflictDialogue(page, repeatedPath);
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const original = await modal.elementHandle();
|
||||
if (original === null) throw new Error("Could not retain the original same-file conflict dialogue");
|
||||
|
||||
await requestRepeatedConflictChecks(cliBinary, repeatedSession.cliEnv, repeatedPath, 3);
|
||||
await page.waitForFunction((element) => !element.isConnected, original, { timeout: uiTimeoutMs });
|
||||
await modal.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await page.waitForTimeout(250);
|
||||
const visibleCount = await conflictDialogue(page, repeatedPath).evaluateAll(
|
||||
(elements) => elements.filter((element) => element.getClientRects().length > 0).length
|
||||
);
|
||||
if (visibleCount !== 1) {
|
||||
throw new Error(`Expected one newest same-file dialogue, but found ${visibleCount}`);
|
||||
}
|
||||
});
|
||||
const repeatedDialogueScreenshot = await captureObsidianElement(
|
||||
session.remoteDebuggingPort,
|
||||
"conflict-dialog-same-file-replacement.png",
|
||||
(page) => conflictDialogue(page, repeatedPath).locator(".modal").first()
|
||||
);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
const modal = conflictDialogue(page, repeatedPath);
|
||||
await modal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
});
|
||||
await waitForConflictChecks(cliBinary, session.cliEnv);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await page.waitForTimeout(500);
|
||||
if (await conflictDialogue(page, repeatedPath).isVisible()) {
|
||||
throw new Error("A superseded same-file conflict dialogue opened after the newest request completed");
|
||||
}
|
||||
});
|
||||
|
||||
await createTwoVersionConflict(cliBinary, session.cliEnv, activePath);
|
||||
await createTwoVersionConflict(cliBinary, session.cliEnv, waitingPath);
|
||||
const externallyResolvedWaitingFixture = await createTwoVersionConflict(
|
||||
cliBinary,
|
||||
session.cliEnv,
|
||||
externallyResolvedWaitingPath
|
||||
);
|
||||
await requestConflictCheck(cliBinary, session.cliEnv, false, activePath);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await conflictDialogue(page, activePath).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
});
|
||||
await requestConflictCheck(cliBinary, session.cliEnv, false, waitingPath);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await page.waitForTimeout(500);
|
||||
if (!(await conflictDialogue(page, activePath).isVisible())) {
|
||||
throw new Error("A different-file request closed the active conflict dialogue");
|
||||
}
|
||||
if (await conflictDialogue(page, waitingPath).isVisible()) {
|
||||
throw new Error("A different-file conflict dialogue opened before the active dialogue completed");
|
||||
}
|
||||
});
|
||||
const differentFileWaitingScreenshot = await captureObsidianElement(
|
||||
session.remoteDebuggingPort,
|
||||
"conflict-dialog-different-file-waiting.png",
|
||||
(page) => conflictDialogue(page, activePath).locator(".modal").first()
|
||||
);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
const activeModal = conflictDialogue(page, activePath);
|
||||
await activeModal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await activeModal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
await conflictDialogue(page, waitingPath).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
await requestInteractiveConflictResolution(
|
||||
cliBinary,
|
||||
session.cliEnv,
|
||||
externallyResolvedWaitingPath,
|
||||
externallyResolvedWaitingFixture,
|
||||
true
|
||||
);
|
||||
await applyReplicatedConflictResolution(
|
||||
cliBinary,
|
||||
session.cliEnv,
|
||||
externallyResolvedWaitingFixture.conflicts[0],
|
||||
0,
|
||||
externallyResolvedWaitingPath
|
||||
);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
if (!(await conflictDialogue(page, waitingPath).isVisible())) {
|
||||
throw new Error("Resolving a waiting file elsewhere closed the active different-file dialogue");
|
||||
}
|
||||
if (await conflictDialogue(page, externallyResolvedWaitingPath).isVisible()) {
|
||||
throw new Error("A conflict dialogue opened for a waiting file which was already resolved elsewhere");
|
||||
}
|
||||
const waitingModal = conflictDialogue(page, waitingPath);
|
||||
await waitingModal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await waitingModal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
await page.waitForFunction(
|
||||
() => (globalThis as ObsidianTestGlobal).__livesyncWaitingConflictCompleted === true,
|
||||
undefined,
|
||||
{ timeout: uiTimeoutMs }
|
||||
);
|
||||
await page.waitForTimeout(250);
|
||||
if (await conflictDialogue(page, externallyResolvedWaitingPath).isVisible()) {
|
||||
throw new Error("A resolved waiting-file dialogue opened after the active dialogue completed");
|
||||
}
|
||||
});
|
||||
await waitForConflictChecks(cliBinary, session.cliEnv);
|
||||
|
||||
await createTwoVersionConflict(cliBinary, session.cliEnv, unloadActivePath);
|
||||
const unloadWaitingFixture = await createTwoVersionConflict(cliBinary, session.cliEnv, unloadWaitingPath);
|
||||
await requestConflictCheck(cliBinary, session.cliEnv, false, unloadActivePath);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await conflictDialogue(page, unloadActivePath).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
});
|
||||
await requestInteractiveConflictResolution(
|
||||
cliBinary,
|
||||
session.cliEnv,
|
||||
unloadWaitingPath,
|
||||
unloadWaitingFixture,
|
||||
true
|
||||
);
|
||||
const beforeUnloadScreenshot = await captureObsidianElement(
|
||||
session.remoteDebuggingPort,
|
||||
"conflict-dialog-before-unload.png",
|
||||
(page) => conflictDialogue(page, unloadActivePath).locator(".modal").first()
|
||||
);
|
||||
await disableLiveSyncAndWaitForConflictChecks(session.remoteDebuggingPort);
|
||||
const afterUnloadScreenshot = await captureObsidianPage(
|
||||
session.remoteDebuggingPort,
|
||||
"conflict-dialog-after-unload.png",
|
||||
async (page) => {
|
||||
await page.waitForTimeout(250);
|
||||
if (await conflictDialogue(page).isVisible()) {
|
||||
throw new Error("A conflict dialogue remained visible after LiveSync was unloaded");
|
||||
}
|
||||
const pluginStillLoaded = await page.evaluate(
|
||||
() => (globalThis as ObsidianTestGlobal).app?.plugins?.plugins?.["obsidian-livesync"] !== undefined
|
||||
);
|
||||
if (pluginStillLoaded) throw new Error("LiveSync remained loaded after disablePlugin completed");
|
||||
}
|
||||
);
|
||||
|
||||
await enableLiveSync(session.remoteDebuggingPort);
|
||||
await waitForLiveSyncCoreReady(cliBinary, session.cliEnv);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await page.waitForTimeout(500);
|
||||
if (await conflictDialogue(page).isVisible()) {
|
||||
throw new Error("A stale conflict dialogue reopened after LiveSync was enabled again");
|
||||
}
|
||||
});
|
||||
await createAndOpenBaseFile(cliBinary, session.cliEnv, unloadActivePath);
|
||||
await requestConflictCheck(cliBinary, session.cliEnv, false, unloadActivePath);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
await conflictDialogue(page, unloadActivePath).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
});
|
||||
const afterReloadScreenshot = await captureObsidianElement(
|
||||
session.remoteDebuggingPort,
|
||||
"conflict-dialog-after-reload.png",
|
||||
(page) => conflictDialogue(page, unloadActivePath).locator(".modal").first()
|
||||
);
|
||||
await withObsidianPage(session.remoteDebuggingPort, async (page) => {
|
||||
const modal = conflictDialogue(page, unloadActivePath);
|
||||
await modal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs });
|
||||
});
|
||||
await waitForConflictChecks(cliBinary, session.cliEnv);
|
||||
|
||||
console.log(
|
||||
"Real Obsidian reviewed three versions pairwise, retained the completed stage across restart, suppressed an ordinary repeat prompt after Not now, reopened the dialogue after the explicit command, and cleared both postponed and open-dialogue states after replicated resolutions."
|
||||
"Real Obsidian preserved pairwise conflict resolution and dialogue presentation; replaced only stale same-file dialogues; serialised different files; discarded externally resolved waiting requests; and drained active and waiting requests across unload and reload."
|
||||
);
|
||||
console.log(`Dialogue screenshot: ${firstDialogueScreenshot}`);
|
||||
console.log(`Postponed warning screenshot: ${warningScreenshot}`);
|
||||
console.log(`Same-file replacement screenshot: ${repeatedDialogueScreenshot}`);
|
||||
console.log(`Different-file waiting screenshot: ${differentFileWaitingScreenshot}`);
|
||||
console.log(`Before unload screenshot: ${beforeUnloadScreenshot}`);
|
||||
console.log(`After unload screenshot: ${afterUnloadScreenshot}`);
|
||||
console.log(`After reload screenshot: ${afterReloadScreenshot}`);
|
||||
} finally {
|
||||
if (session) {
|
||||
await session.app.stop();
|
||||
|
||||
@@ -15,6 +15,7 @@ const testSteps: Step[] = [
|
||||
{ name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] },
|
||||
{ name: "onboarding invitation", args: ["run", "test:e2e:obsidian:onboarding-invitation"] },
|
||||
{ name: "Svelte dialogue mounts", args: ["run", "test:e2e:obsidian:dialog-mounts"] },
|
||||
{ name: "conflict dialogue policy", args: ["run", "test:e2e:obsidian:conflict-dialog-policy"] },
|
||||
{ name: "revision repair", args: ["run", "test:e2e:obsidian:revision-repair"] },
|
||||
{ name: "settings UI", args: ["run", "test:e2e:obsidian:settings-ui"] },
|
||||
{ name: "Review Harness", args: ["run", "test:e2e:obsidian:review-harness"] },
|
||||
|
||||
@@ -8,6 +8,7 @@ const focusedScenarios = new Set([
|
||||
"smoke",
|
||||
"onboarding-invitation",
|
||||
"dialog-mounts",
|
||||
"conflict-dialog-policy",
|
||||
"revision-repair",
|
||||
"document-history-nav",
|
||||
"document-history-restore",
|
||||
|
||||
Reference in New Issue
Block a user