mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-25 10:33:21 +00:00
test: adopt macOS-safe real-Obsidian sessions
This commit is contained in:
1 parent
0c3c5db6c1
commit
e79686d275
11 files changed
+124
-12
No files matched your search
@@ -76,6 +76,9 @@ jobs:
|
|||||||
- name: Run unit tests suite with coverage
|
- name: Run unit tests suite with coverage
|
||||||
run: npm run test:unit:coverage
|
run: npm run test:unit:coverage
|
||||||
|
|
||||||
|
- name: Run real-Obsidian runner contract tests
|
||||||
|
run: npm run test:e2e:obsidian:runner
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
Generated
+4
-4
@@ -59,7 +59,7 @@
|
|||||||
"@vitest/browser": "^4.1.8",
|
"@vitest/browser": "^4.1.8",
|
||||||
"@vitest/browser-playwright": "^4.1.8",
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
"@vitest/coverage-v8": "^4.1.8",
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
"@vrtmrz/obsidian-test-session": "0.2.1",
|
"@vrtmrz/obsidian-test-session": "0.2.2",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.1",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
@@ -5037,9 +5037,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vrtmrz/obsidian-test-session": {
|
"node_modules/@vrtmrz/obsidian-test-session": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.2.tgz",
|
||||||
"integrity": "sha512-dgT+KZRoHpvc6590pWEUmIm/3jYS+41JDAuFKL+6cjWJN3ZzltbvjiyGgA+8MJjIZ8f5pbadf5zyL+MJxTXCsA==",
|
"integrity": "sha512-wWC7kKjF/2M6cIIIXTXLK4UkPADt3B81jryt3ZANlfpxCniNmXmoEYMVIhPSnZxbQNZqYLuF5RXARf9FxwYELQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+2
-1
@@ -38,6 +38,7 @@
|
|||||||
"test:install-playwright": "npx playwright install chromium",
|
"test:install-playwright": "npx playwright install chromium",
|
||||||
"test:install-dependencies": "npm run test:install-playwright",
|
"test:install-dependencies": "npm run test:install-playwright",
|
||||||
"test:e2e:obsidian:install-appimage": "tsx test/e2e-obsidian/scripts/install-appimage.ts",
|
"test:e2e:obsidian:install-appimage": "tsx test/e2e-obsidian/scripts/install-appimage.ts",
|
||||||
|
"test:e2e:obsidian:runner": "vitest run --config vitest.config.e2e-runner.ts",
|
||||||
"test:e2e:obsidian:discover": "tsx test/e2e-obsidian/scripts/discover.ts",
|
"test:e2e:obsidian:discover": "tsx test/e2e-obsidian/scripts/discover.ts",
|
||||||
"test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts",
|
"test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts",
|
||||||
"test:e2e:obsidian:debug-ui": "tsx test/e2e-obsidian/scripts/debug-ui.ts",
|
"test:e2e:obsidian:debug-ui": "tsx test/e2e-obsidian/scripts/debug-ui.ts",
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
"@vitest/browser": "^4.1.8",
|
"@vitest/browser": "^4.1.8",
|
||||||
"@vitest/browser-playwright": "^4.1.8",
|
"@vitest/browser-playwright": "^4.1.8",
|
||||||
"@vitest/coverage-v8": "^4.1.8",
|
"@vitest/coverage-v8": "^4.1.8",
|
||||||
"@vrtmrz/obsidian-test-session": "0.2.1",
|
"@vrtmrz/obsidian-test-session": "0.2.2",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.1",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Future workflows should use `startObsidianLiveSyncSession()` from `runner/sessio
|
|||||||
|
|
||||||
Each test vault uses an isolated Obsidian profile. The runner creates temporary directories for `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, and Electron `--user-data-dir`, writes the vault registry into those directories, pre-seeds the temporary Chromium local storage so community plug-ins are trusted for that generated vault ID, and passes the same environment to `obsidian-cli`. This is intended to keep real Obsidian E2E runs separate from a developer's daily Obsidian profile and vault registry.
|
Each test vault uses an isolated Obsidian profile. The runner creates temporary directories for `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, and Electron `--user-data-dir`, writes the vault registry into those directories, pre-seeds the temporary Chromium local storage so community plug-ins are trusted for that generated vault ID, and passes the same environment to `obsidian-cli`. This is intended to keep real Obsidian E2E runs separate from a developer's daily Obsidian profile and vault registry.
|
||||||
|
|
||||||
|
On macOS, `@vrtmrz/obsidian-test-session` keeps the generated Vault and profile below `/tmp` so Obsidian's Unix-domain CLI socket remains below the platform path limit. It also gives only the isolated Obsidian process Chromium's mock-keychain flag, preventing the empty test HOME from opening a blocking login-keychain dialogue. LiveSync's deterministic fixture selects the built-in default language so a host-language translation prompt cannot pause plug-in readiness. The case-only rename check enumerates the parent directory and compares exact spellings because an old-path lookup still resolves the renamed file on the default case-insensitive macOS filesystem.
|
||||||
|
|
||||||
## Local Setup
|
## Local Setup
|
||||||
|
|
||||||
Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location.
|
Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location.
|
||||||
@@ -51,6 +53,7 @@ npm run test:contract:contexts
|
|||||||
npm run test:contract:context:webapp
|
npm run test:contract:context:webapp
|
||||||
npm run test:contract:context:cli
|
npm run test:contract:context:cli
|
||||||
npm run test:contract:context:obsidian
|
npm run test:contract:context:obsidian
|
||||||
|
npm run test:e2e:obsidian:runner
|
||||||
npm run test:e2e:obsidian:install-appimage
|
npm run test:e2e:obsidian:install-appimage
|
||||||
npm run test:e2e:obsidian:discover
|
npm run test:e2e:obsidian:discover
|
||||||
npm run test:e2e:obsidian:cli-help -- vaults verbose
|
npm run test:e2e:obsidian:cli-help -- vaults verbose
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { evalObsidianJson } = vi.hoisted(() => ({
|
||||||
|
evalObsidianJson: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./cli.ts", () => ({ evalObsidianJson }));
|
||||||
|
|
||||||
|
import { assertE2eCompatibilityMarker, type CompatibilityMarkerState } from "./liveSyncWorkflow.ts";
|
||||||
|
|
||||||
|
describe("compatibility marker persistence", () => {
|
||||||
|
it("waits for an accepted review to reach device-local storage", async () => {
|
||||||
|
const pending: CompatibilityMarkerState = {
|
||||||
|
vaultName: "fixture",
|
||||||
|
additionalSuffix: "-",
|
||||||
|
expectedStorageKey: "fixture--database-compatibility-version",
|
||||||
|
rawStorageValue: null,
|
||||||
|
serviceValue: "",
|
||||||
|
versionUpFlash: "",
|
||||||
|
};
|
||||||
|
const persisted: CompatibilityMarkerState = {
|
||||||
|
...pending,
|
||||||
|
rawStorageValue: `${VER}`,
|
||||||
|
serviceValue: `${VER}`,
|
||||||
|
};
|
||||||
|
evalObsidianJson.mockResolvedValueOnce(pending).mockResolvedValueOnce(persisted);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
assertE2eCompatibilityMarker("obsidian-cli", {}, { timeoutMs: 100, intervalMs: 0 })
|
||||||
|
).resolves.toEqual(persisted);
|
||||||
|
expect(evalObsidianJson).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,6 +42,11 @@ export type CompatibilityMarkerState = {
|
|||||||
versionUpFlash: string;
|
versionUpFlash: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CompatibilityMarkerWaitOptions = {
|
||||||
|
timeoutMs?: number;
|
||||||
|
intervalMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ResumeCompatibilityReviewOptions = {
|
export type ResumeCompatibilityReviewOptions = {
|
||||||
verifyMissingDeviceMarkerExplanation?: boolean;
|
verifyMissingDeviceMarkerExplanation?: boolean;
|
||||||
screenshotPrefix?: string;
|
screenshotPrefix?: string;
|
||||||
@@ -67,6 +72,7 @@ export type LocalDatabaseEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const E2E_PREFERRED_SETTINGS = {
|
const E2E_PREFERRED_SETTINGS = {
|
||||||
|
displayLanguage: "def",
|
||||||
liveSync: false,
|
liveSync: false,
|
||||||
syncOnStart: false,
|
syncOnStart: false,
|
||||||
syncOnSave: false,
|
syncOnSave: false,
|
||||||
@@ -127,14 +133,19 @@ export async function readE2eCompatibilityMarker(
|
|||||||
|
|
||||||
export async function assertE2eCompatibilityMarker(
|
export async function assertE2eCompatibilityMarker(
|
||||||
cliBinary: string,
|
cliBinary: string,
|
||||||
env: NodeJS.ProcessEnv
|
env: NodeJS.ProcessEnv,
|
||||||
|
options: CompatibilityMarkerWaitOptions = {}
|
||||||
): Promise<CompatibilityMarkerState> {
|
): Promise<CompatibilityMarkerState> {
|
||||||
const state = await readE2eCompatibilityMarker(cliBinary, env);
|
const timeoutMs = options.timeoutMs ?? Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000);
|
||||||
if (state.serviceValue !== `${VER}`) {
|
const intervalMs = options.intervalMs ?? 100;
|
||||||
throw new Error(
|
const deadline = Date.now() + timeoutMs;
|
||||||
`The E2E compatibility marker was not available on first plug-in load: ${JSON.stringify(state)}`
|
let state = await readE2eCompatibilityMarker(cliBinary, env);
|
||||||
);
|
while (state.serviceValue !== `${VER}` && Date.now() < deadline) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
|
state = await readE2eCompatibilityMarker(cliBinary, env);
|
||||||
}
|
}
|
||||||
|
if (state.serviceValue !== `${VER}`)
|
||||||
|
throw new Error(`The E2E compatibility marker was not persisted before timeout: ${JSON.stringify(state)}`);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { hasExactCaseOnlyRename } from "./pathEntries.ts";
|
||||||
|
|
||||||
|
describe("case-only rename assertions", () => {
|
||||||
|
it("accepts only the exact new spelling", () => {
|
||||||
|
expect(hasExactCaseOnlyRename(["case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects the old spelling even when a case-insensitive lookup would resolve it", () => {
|
||||||
|
expect(hasExactCaseOnlyRename(["Case-Rename.md"], "Case-Rename.md", "case-rename.md")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an ambiguous directory containing both spellings", () => {
|
||||||
|
expect(hasExactCaseOnlyRename(["Case-Rename.md", "case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { readdir } from "node:fs/promises";
|
||||||
|
import { basename, dirname, join } from "node:path";
|
||||||
|
import { hasExactCaseOnlyRename } from "./pathEntries.ts";
|
||||||
|
|
||||||
|
export async function waitForExactCaseOnlyRename(
|
||||||
|
vaultPath: string,
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 10000)
|
||||||
|
): Promise<void> {
|
||||||
|
const oldDirectory = dirname(oldPath);
|
||||||
|
const newDirectory = dirname(newPath);
|
||||||
|
if (oldDirectory !== newDirectory) {
|
||||||
|
throw new Error(`Case-only rename paths must share one parent directory: ${oldPath} -> ${newPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldName = basename(oldPath);
|
||||||
|
const newName = basename(newPath);
|
||||||
|
const directoryPath = join(vaultPath, newDirectory);
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
let lastEntries: string[] = [];
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
lastEntries = await readdir(directoryPath);
|
||||||
|
if (hasExactCaseOnlyRename(lastEntries, oldName, newName)) return;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Timed out waiting for exact case-only rename: ${oldPath} -> ${newPath}. Directory entries: ${JSON.stringify(lastEntries)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function hasExactCaseOnlyRename(entries: readonly string[], oldName: string, newName: string): boolean {
|
||||||
|
return entries.includes(newName) && !entries.includes(oldName);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type CouchDbConfig,
|
type CouchDbConfig,
|
||||||
} from "../runner/couchdb.ts";
|
} from "../runner/couchdb.ts";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
|
import { waitForExactCaseOnlyRename } from "../runner/pathAssertions.ts";
|
||||||
import {
|
import {
|
||||||
assertEqual,
|
assertEqual,
|
||||||
assertE2eCompatibilityMarker,
|
assertE2eCompatibilityMarker,
|
||||||
@@ -404,7 +405,7 @@ async function runCaseOnlyRename(
|
|||||||
session = await startConfiguredSession(context, vaultB);
|
session = await startConfiguredSession(context, vaultB);
|
||||||
await syncAndApply(context, session);
|
await syncAndApply(context, session);
|
||||||
const renamedOnB = await waitForPathContent(vaultB.path, caseRenameToPath, (content) => content === fileContent);
|
const renamedOnB = await waitForPathContent(vaultB.path, caseRenameToPath, (content) => content === fileContent);
|
||||||
await waitForPathDeleted(vaultB.path, caseRenameFromPath);
|
await waitForExactCaseOnlyRename(vaultB.path, caseRenameFromPath, caseRenameToPath);
|
||||||
await session.app.stop();
|
await session.app.stop();
|
||||||
|
|
||||||
assertEqual(renamedOnB, fileContent, "Case-only note rename did not round-trip to the second vault.");
|
assertEqual(renamedOnB, fileContent, "Case-only note rename did not round-trip to the second vault.");
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
include: ["test/e2e-obsidian/runner/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in new issue
Block a user