obsidian-e2e 0.0.0-next.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/vitest.d.mts +38 -1
- package/dist/vitest.mjs +193 -5
- package/dist/vitest.mjs.map +1 -1
- package/package.json +22 -3
package/README.md
CHANGED
|
@@ -62,6 +62,76 @@ Run Obsidian-backed tests serially. A live Obsidian app and shared vault are
|
|
|
62
62
|
not safe to hit from multiple Vitest workers at once, so `fileParallelism: false`
|
|
63
63
|
and `maxWorkers: 1` should be treated as the default, not as an optimization.
|
|
64
64
|
|
|
65
|
+
## Shared Vault Locking
|
|
66
|
+
|
|
67
|
+
If multiple worktrees or separate test runs all point at the same
|
|
68
|
+
`obsidian vault=dev` vault, you can enable `sharedVaultLock` to serialize access
|
|
69
|
+
across those runs:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createObsidianTest, createPluginTest } from "obsidian-e2e/vitest";
|
|
73
|
+
|
|
74
|
+
export const test = createObsidianTest({
|
|
75
|
+
vault: "dev",
|
|
76
|
+
sharedVaultLock: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const pluginTest = createPluginTest({
|
|
80
|
+
vault: "dev",
|
|
81
|
+
pluginId: "quickadd",
|
|
82
|
+
sharedVaultLock: {
|
|
83
|
+
onBusy: "wait",
|
|
84
|
+
timeoutMs: 60_000,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`sharedVaultLock` is acquired once per worker before that worker starts using
|
|
90
|
+
the shared vault. The authoritative state is a host-side lock directory keyed
|
|
91
|
+
by the resolved vault path. That file-backed lock owns the lease, updates a
|
|
92
|
+
heartbeat, and allows stale-lock takeover after the configured timeout window.
|
|
93
|
+
|
|
94
|
+
For visibility inside the running app, the holder also publishes a best-effort
|
|
95
|
+
marker into the Obsidian process. That marker is not authoritative. The
|
|
96
|
+
filesystem lock is the source of truth, and the app marker is only there to
|
|
97
|
+
help humans understand which run currently owns the vault.
|
|
98
|
+
|
|
99
|
+
For lock diagnostics, `obsidian-e2e/vitest` also exports:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { inspectVaultRunLock, readVaultRunLockMarker } from "obsidian-e2e/vitest";
|
|
103
|
+
|
|
104
|
+
const state = await inspectVaultRunLock({
|
|
105
|
+
vaultPath: "/absolute/path/to/dev-vault",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const marker = await readVaultRunLockMarker(obsidian);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`inspectVaultRunLock()` reads the authoritative host-side lock state and
|
|
112
|
+
returns the current metadata, lock directory, heartbeat age, and stale status.
|
|
113
|
+
`readVaultRunLockMarker()` reads the best-effort marker from the running
|
|
114
|
+
Obsidian app.
|
|
115
|
+
|
|
116
|
+
Within one worker/process, reacquiring the same shared-vault lock is reentrant:
|
|
117
|
+
the existing lease is reused instead of contending against itself. Across
|
|
118
|
+
different processes or worktrees, contention still serializes access through
|
|
119
|
+
the host-side lock.
|
|
120
|
+
|
|
121
|
+
The lock path is covered by a real multi-process smoke test: one process can
|
|
122
|
+
hold the lease while another waits, and a second process can also take over
|
|
123
|
+
after the original holder dies and its heartbeat goes stale.
|
|
124
|
+
|
|
125
|
+
The fixture layer is also covered the same way: separate `createObsidianTest()`
|
|
126
|
+
runs can contend for the same `sharedVaultLock`, and the smoke path verifies
|
|
127
|
+
that one run waits until the other releases or goes stale. That proves lock
|
|
128
|
+
handoff across process boundaries, not safe parallel mutation inside one vault.
|
|
129
|
+
|
|
130
|
+
This mode prevents collisions between concurrent runs that share one live
|
|
131
|
+
vault, but it does not create true parallel execution inside that vault. It
|
|
132
|
+
serializes access. If your goal is real parallelism, use separate vaults rather
|
|
133
|
+
than one shared `vault: "dev"` target.
|
|
134
|
+
|
|
65
135
|
## Writing Tests
|
|
66
136
|
|
|
67
137
|
```ts
|
package/dist/vitest.d.mts
CHANGED
|
@@ -10,9 +10,17 @@ interface FailureArtifactOptions {
|
|
|
10
10
|
tabs?: boolean;
|
|
11
11
|
workspace?: boolean;
|
|
12
12
|
}
|
|
13
|
+
interface SharedVaultLockOptions {
|
|
14
|
+
heartbeatMs?: number;
|
|
15
|
+
lockRoot?: string;
|
|
16
|
+
onBusy?: "fail" | "wait";
|
|
17
|
+
staleMs?: number;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
13
20
|
interface CreateObsidianTestOptions extends CreateObsidianClientOptions {
|
|
14
21
|
artifactsDir?: string;
|
|
15
22
|
captureOnFailure?: boolean | FailureArtifactOptions;
|
|
23
|
+
sharedVaultLock?: boolean | SharedVaultLockOptions;
|
|
16
24
|
sandboxRoot?: string;
|
|
17
25
|
}
|
|
18
26
|
type VaultSeedEntry = string | {
|
|
@@ -42,5 +50,34 @@ declare function createObsidianTest(options: CreateObsidianTestOptions): Obsidia
|
|
|
42
50
|
//#region src/fixtures/create-plugin-test.d.ts
|
|
43
51
|
declare function createPluginTest(options: CreatePluginTestOptions): PluginTest;
|
|
44
52
|
//#endregion
|
|
45
|
-
|
|
53
|
+
//#region src/fixtures/vault-lock.d.ts
|
|
54
|
+
interface VaultRunLockMetadata {
|
|
55
|
+
acquiredAt: number;
|
|
56
|
+
cwd: string;
|
|
57
|
+
heartbeatAt: number;
|
|
58
|
+
hostname: string;
|
|
59
|
+
ownerId: string;
|
|
60
|
+
pid: number;
|
|
61
|
+
staleMs: number;
|
|
62
|
+
vaultName: string;
|
|
63
|
+
vaultPath: string;
|
|
64
|
+
}
|
|
65
|
+
interface VaultRunLockState {
|
|
66
|
+
heartbeatAgeMs: number;
|
|
67
|
+
isStale: boolean;
|
|
68
|
+
lockDir: string;
|
|
69
|
+
metadata: VaultRunLockMetadata;
|
|
70
|
+
}
|
|
71
|
+
interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {
|
|
72
|
+
vaultName: string;
|
|
73
|
+
vaultPath: string;
|
|
74
|
+
}
|
|
75
|
+
declare function inspectVaultRunLock({
|
|
76
|
+
lockRoot,
|
|
77
|
+
staleMs,
|
|
78
|
+
vaultPath
|
|
79
|
+
}: Pick<AcquireVaultRunLockOptions, "lockRoot" | "staleMs" | "vaultPath">): Promise<VaultRunLockState | null>;
|
|
80
|
+
declare function readVaultRunLockMarker(obsidian: ObsidianClient): Promise<VaultRunLockMetadata | null>;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { type CreateObsidianTestOptions, type CreatePluginTestOptions, type ObsidianFixtures, type ObsidianTest, type PluginFixtures, type PluginTest, type SharedVaultLockOptions, type VaultRunLockMetadata, type VaultRunLockState, type VaultSeed, type VaultSeedEntry, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
|
|
46
83
|
//# sourceMappingURL=vitest.d.mts.map
|
package/dist/vitest.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { i as getClientInternals, n as createVaultApi, r as createObsidianClient, t as createSandboxApi } from "./sandbox-BhesE1S4.mjs";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
5
|
import { test } from "vite-plus/test";
|
|
6
|
+
import os from "node:os";
|
|
5
7
|
//#region src/fixtures/failure-artifacts.ts
|
|
6
8
|
const DEFAULT_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
|
|
7
9
|
function getFailureArtifactConfig(options) {
|
|
@@ -95,12 +97,197 @@ function formatArtifactError(error) {
|
|
|
95
97
|
function sanitizeForPath(value) {
|
|
96
98
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
|
|
97
99
|
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/fixtures/vault-lock.ts
|
|
102
|
+
const DEFAULT_HEARTBEAT_MS = 2e3;
|
|
103
|
+
const DEFAULT_STALE_MS = 15e3;
|
|
104
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
105
|
+
const DEFAULT_WAIT_INTERVAL_MS = 500;
|
|
106
|
+
const DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), "obsidian-e2e-locks");
|
|
107
|
+
const LOCK_METADATA_FILE = "lock.json";
|
|
108
|
+
const APP_LOCK_KEY = "__obsidianE2ELock";
|
|
109
|
+
const heldLocks = /* @__PURE__ */ new Map();
|
|
110
|
+
async function acquireVaultRunLock({ heartbeatMs = DEFAULT_HEARTBEAT_MS, lockRoot = DEFAULT_LOCK_ROOT, onBusy = "wait", staleMs = DEFAULT_STALE_MS, timeoutMs = DEFAULT_TIMEOUT_MS, vaultName, vaultPath }) {
|
|
111
|
+
const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
|
|
112
|
+
const heldLock = heldLocks.get(lockDir);
|
|
113
|
+
if (heldLock) {
|
|
114
|
+
heldLock.refs += 1;
|
|
115
|
+
return createVaultRunLockHandle(heldLock);
|
|
116
|
+
}
|
|
117
|
+
const ownerId = randomUUID();
|
|
118
|
+
const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
|
|
119
|
+
const metadata = {
|
|
120
|
+
acquiredAt: Date.now(),
|
|
121
|
+
cwd: process.cwd(),
|
|
122
|
+
heartbeatAt: Date.now(),
|
|
123
|
+
hostname: os.hostname(),
|
|
124
|
+
ownerId,
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
staleMs,
|
|
127
|
+
vaultName,
|
|
128
|
+
vaultPath
|
|
129
|
+
};
|
|
130
|
+
await mkdir(lockRoot, { recursive: true });
|
|
131
|
+
const startedAt = Date.now();
|
|
132
|
+
while (true) try {
|
|
133
|
+
await mkdir(lockDir);
|
|
134
|
+
await writeMetadata(metadataPath, metadata);
|
|
135
|
+
break;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!isAlreadyExistsError(error)) throw error;
|
|
138
|
+
const currentLock = await inspectVaultRunLock({
|
|
139
|
+
lockRoot,
|
|
140
|
+
staleMs,
|
|
141
|
+
vaultPath
|
|
142
|
+
});
|
|
143
|
+
if (currentLock && !currentLock.isStale) {
|
|
144
|
+
if (onBusy === "fail") throw new Error(formatBusyLockMessage(vaultPath, currentLock));
|
|
145
|
+
} else {
|
|
146
|
+
await rm(lockDir, {
|
|
147
|
+
force: true,
|
|
148
|
+
recursive: true
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (Date.now() - startedAt >= timeoutMs) throw new Error(currentLock ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}` : `Timed out waiting for shared vault lock on ${vaultPath}`);
|
|
153
|
+
await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));
|
|
154
|
+
}
|
|
155
|
+
const heartbeat = setInterval(() => {
|
|
156
|
+
metadata.heartbeatAt = Date.now();
|
|
157
|
+
writeMetadata(metadataPath, metadata).catch(() => {});
|
|
158
|
+
}, heartbeatMs);
|
|
159
|
+
heartbeat.unref();
|
|
160
|
+
const nextHeldLock = {
|
|
161
|
+
heartbeat,
|
|
162
|
+
lockDir,
|
|
163
|
+
metadata,
|
|
164
|
+
metadataPath,
|
|
165
|
+
refs: 1
|
|
166
|
+
};
|
|
167
|
+
heldLocks.set(lockDir, nextHeldLock);
|
|
168
|
+
return createVaultRunLockHandle(nextHeldLock);
|
|
169
|
+
}
|
|
170
|
+
async function clearVaultRunLockMarker(obsidian) {
|
|
171
|
+
await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; "cleared"`, { allowNonZeroExit: true });
|
|
172
|
+
}
|
|
173
|
+
async function inspectVaultRunLock({ lockRoot = DEFAULT_LOCK_ROOT, staleMs = DEFAULT_STALE_MS, vaultPath }) {
|
|
174
|
+
const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
|
|
175
|
+
const metadata = await readLockState(lockDir);
|
|
176
|
+
if (!metadata) return null;
|
|
177
|
+
return {
|
|
178
|
+
heartbeatAgeMs: Date.now() - metadata.heartbeatAt,
|
|
179
|
+
isStale: isLockStale(metadata, staleMs),
|
|
180
|
+
lockDir,
|
|
181
|
+
metadata
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function readVaultRunLockMarker(obsidian) {
|
|
185
|
+
return obsidian.dev.eval(`window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`, { allowNonZeroExit: true });
|
|
186
|
+
}
|
|
187
|
+
function createVaultLockKey(vaultPath) {
|
|
188
|
+
return createHash("sha256").update(path.resolve(vaultPath)).digest("hex");
|
|
189
|
+
}
|
|
190
|
+
function buildSetMarkerCode(metadata) {
|
|
191
|
+
return `(() => {
|
|
192
|
+
const lock = ${JSON.stringify(metadata)};
|
|
193
|
+
window.${APP_LOCK_KEY} = lock;
|
|
194
|
+
app.${APP_LOCK_KEY} = lock;
|
|
195
|
+
return lock;
|
|
196
|
+
})()`;
|
|
197
|
+
}
|
|
198
|
+
function createVaultRunLockHandle(heldLock) {
|
|
199
|
+
return {
|
|
200
|
+
get lockDir() {
|
|
201
|
+
return heldLock.lockDir;
|
|
202
|
+
},
|
|
203
|
+
get metadata() {
|
|
204
|
+
return heldLock.metadata;
|
|
205
|
+
},
|
|
206
|
+
async publishMarker(obsidian) {
|
|
207
|
+
await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));
|
|
208
|
+
},
|
|
209
|
+
async release() {
|
|
210
|
+
if (heldLock.refs > 1) {
|
|
211
|
+
heldLock.refs -= 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
heldLocks.delete(heldLock.lockDir);
|
|
215
|
+
clearInterval(heldLock.heartbeat);
|
|
216
|
+
if ((await readLockState(heldLock.lockDir))?.ownerId !== heldLock.metadata.ownerId) return;
|
|
217
|
+
await rm(heldLock.lockDir, {
|
|
218
|
+
force: true,
|
|
219
|
+
recursive: true
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function readLockState(lockDir) {
|
|
225
|
+
const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(await readFile(metadataPath, "utf8"));
|
|
228
|
+
} catch {
|
|
229
|
+
try {
|
|
230
|
+
const directoryStat = await stat(lockDir);
|
|
231
|
+
return {
|
|
232
|
+
acquiredAt: directoryStat.mtimeMs,
|
|
233
|
+
cwd: "",
|
|
234
|
+
heartbeatAt: directoryStat.mtimeMs,
|
|
235
|
+
hostname: "",
|
|
236
|
+
ownerId: "",
|
|
237
|
+
pid: 0,
|
|
238
|
+
staleMs: DEFAULT_STALE_MS,
|
|
239
|
+
vaultName: "",
|
|
240
|
+
vaultPath: ""
|
|
241
|
+
};
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function formatBusyLockMessage(vaultPath, state) {
|
|
248
|
+
return `vault ${vaultPath} is locked by ${state.metadata.ownerId ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || "<unknown>"}` : "owner=<unknown>"} ${`heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`}`;
|
|
249
|
+
}
|
|
250
|
+
function isAlreadyExistsError(error) {
|
|
251
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
252
|
+
}
|
|
253
|
+
function isLockStale(metadata, staleMs) {
|
|
254
|
+
return Date.now() - metadata.heartbeatAt > staleMs;
|
|
255
|
+
}
|
|
256
|
+
async function sleep(durationMs) {
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
258
|
+
}
|
|
259
|
+
async function writeMetadata(metadataPath, metadata) {
|
|
260
|
+
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
261
|
+
}
|
|
98
262
|
function createBaseFixtures(options, fixtureOptions = {}) {
|
|
99
263
|
const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
|
|
100
264
|
return {
|
|
101
|
-
|
|
265
|
+
_vaultLock: [async ({}, use) => {
|
|
266
|
+
if (!options.sharedVaultLock) {
|
|
267
|
+
await use(null);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const lockClient = createObsidianClient(options);
|
|
271
|
+
await lockClient.verify();
|
|
272
|
+
const vaultLock = await acquireVaultRunLock({
|
|
273
|
+
...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
|
|
274
|
+
vaultName: options.vault,
|
|
275
|
+
vaultPath: await lockClient.vaultPath()
|
|
276
|
+
});
|
|
277
|
+
await vaultLock.publishMarker(lockClient);
|
|
278
|
+
try {
|
|
279
|
+
await use(vaultLock);
|
|
280
|
+
} finally {
|
|
281
|
+
try {
|
|
282
|
+
await clearVaultRunLockMarker(lockClient);
|
|
283
|
+
} catch {}
|
|
284
|
+
await vaultLock.release();
|
|
285
|
+
}
|
|
286
|
+
}, { scope: "worker" }],
|
|
287
|
+
obsidian: async ({ _vaultLock, onTestFailed, task }, use) => {
|
|
102
288
|
const obsidian = createObsidianClient(options);
|
|
103
289
|
await obsidian.verify();
|
|
290
|
+
if (_vaultLock) await _vaultLock.publishMarker(obsidian);
|
|
104
291
|
registerFailureArtifacts({
|
|
105
292
|
onTestFailed,
|
|
106
293
|
task
|
|
@@ -136,7 +323,7 @@ function createObsidianTest(options) {
|
|
|
136
323
|
//#endregion
|
|
137
324
|
//#region src/fixtures/create-plugin-test.ts
|
|
138
325
|
function createPluginTest(options) {
|
|
139
|
-
|
|
326
|
+
const fixtures = {
|
|
140
327
|
...createBaseFixtures(options, { async createVault(obsidian) {
|
|
141
328
|
if (options.seedVault) await applyVaultSeed(obsidian, options.seedVault);
|
|
142
329
|
return createVaultApi({ obsidian });
|
|
@@ -156,7 +343,8 @@ function createPluginTest(options) {
|
|
|
156
343
|
if (!wasEnabled) await plugin.disable({ filter: options.pluginFilter });
|
|
157
344
|
}
|
|
158
345
|
}
|
|
159
|
-
}
|
|
346
|
+
};
|
|
347
|
+
return test.extend(fixtures);
|
|
160
348
|
}
|
|
161
349
|
async function applyVaultSeed(obsidian, seedVault) {
|
|
162
350
|
const vaultRoot = await obsidian.vaultPath();
|
|
@@ -177,6 +365,6 @@ async function writeSeedValue(resolvedPath, value) {
|
|
|
177
365
|
await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\n`, "utf8");
|
|
178
366
|
}
|
|
179
367
|
//#endregion
|
|
180
|
-
export { createObsidianTest, createPluginTest };
|
|
368
|
+
export { createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
|
|
181
369
|
|
|
182
370
|
//# sourceMappingURL=vitest.mjs.map
|
package/dist/vitest.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { TestContext } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions, FailureArtifactOptions } from \"./types\";\n\nconst DEFAULT_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\ninterface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport function getFailureArtifactConfig(\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: Pick<TestContext[\"task\"], \"id\" | \"name\">,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(\n artifactDirectory,\n \"editor.json\",\n config.capture.editorText,\n async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n }),\n ),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () =>\n obsidian.tabs(),\n ),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n ]);\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n });\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n { onTestFailed, task }: Pick<TestContext, \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures>(createBaseFixtures(options));\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { createBaseFixtures } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n return base.extend<PluginFixtures>({\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n });\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vaultRoot = await obsidian.vaultPath();\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = path.resolve(vaultRoot, ...targetPath.split(\"/\").filter(Boolean));\n const normalizedVaultRoot = path.resolve(vaultRoot);\n\n if (\n resolvedPath !== normalizedVaultRoot &&\n !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)\n ) {\n throw new Error(`Seed path escapes the vault root: ${targetPath}`);\n }\n\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeSeedValue(resolvedPath, value);\n }\n}\n\nasync function writeSeedValue(resolvedPath: string, value: VaultSeedEntry): Promise<void> {\n if (typeof value === \"string\") {\n await writeFile(resolvedPath, value, \"utf8\");\n return;\n }\n\n await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":";;;;;AAQA,MAAM,wBAAwB;AAQ9B,SAAgB,yBACd,SACuB;AACvB,KAAI,CAAC,QAAQ,iBACX,QAAO;EACL,cAAc,KAAK,QAAQ,QAAQ,gBAAgB,sBAAsB;EACzE,SAAS;GACP,YAAY;GACZ,KAAK;GACL,YAAY;GACZ,YAAY;GACZ,MAAM;GACN,WAAW;GACZ;EACD,SAAS;EACV;CAGH,MAAM,YAAY,QAAQ,qBAAqB,OAAO,EAAE,GAAG,QAAQ;AAEnE,QAAO;EACL,cAAc,KAAK,QAAQ,QAAQ,gBAAgB,sBAAsB;EACzE,SAAS;GACP,YAAY,UAAU,cAAc;GACpC,KAAK,UAAU,OAAO;GACtB,YAAY,UAAU,cAAc;GACpC,YAAY,UAAU,cAAc;GACpC,MAAM,UAAU,QAAQ;GACxB,WAAW,UAAU,aAAa;GACnC;EACD,SAAS;EACV;;AAGH,SAAgB,4BACd,cACA,MACQ;CACR,MAAM,SAAS,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,GAAG,IAAI;AAC5C,QAAO,KAAK,KAAK,cAAc,GAAG,gBAAgB,KAAK,KAAK,CAAC,GAAG,SAAS;;AAG3E,SAAgB,yBACd,SACA,UACA,SACM;CACN,MAAM,SAAS,yBAAyB,QAAQ;AAEhD,KAAI,CAAC,OAAO,QACV;AAGF,SAAQ,aAAa,YAAY;EAC/B,MAAM,oBAAoB,4BAA4B,OAAO,cAAc,QAAQ,KAAK;AACxF,QAAM,MAAM,mBAAmB,EAAE,WAAW,MAAM,CAAC;AAEnD,QAAM,QAAQ,IAAI;GAChB,oBACE,mBACA,oBACA,OAAO,QAAQ,YACf,aAAa,EACX,YAAY,MAAM,SAAS,IAAI,KAC7B,8CACD,EACF,EACF;GACD,oBAAoB,mBAAmB,WAAW,OAAO,QAAQ,KAAK,YACpE,OACE,MAAM,SAAS,IAAI,IAAI;IACrB,OAAO;IACP,UAAU;IACX,CAAC,CACH,CACF;GACD,oBACE,mBACA,eACA,OAAO,QAAQ,YACf,aAAa,EACX,MAAM,MAAM,SAAS,IAAI,KACvB,+DACD,EACF,EACF;GACD,0BAA0B,mBAAmB,OAAO,QAAQ,YAAY,SAAS;GACjF,oBAAoB,mBAAmB,aAAa,OAAO,QAAQ,YACjE,SAAS,MAAM,CAChB;GACD,oBAAoB,mBAAmB,kBAAkB,OAAO,QAAQ,iBACtE,SAAS,WAAW,CACrB;GACF,CAAC;GACF;;AAGJ,SAAgB,+BACd,SACA,QACA,SACM;CACN,MAAM,SAAS,yBAAyB,QAAQ;AAEhD,KAAI,CAAC,OAAO,QACV;AAGF,SAAQ,aAAa,YAAY;EAC/B,MAAM,oBAAoB,4BAA4B,OAAO,cAAc,QAAQ,KAAK;AACxF,QAAM,MAAM,mBAAmB,EAAE,WAAW,MAAM,CAAC;AACnD,QAAM,oBAAoB,mBAAmB,GAAG,OAAO,GAAG,aAAa,YACrE,OAAO,MAAM,CAAC,MAAM,CACrB;GACD;;AAGJ,eAAe,oBACb,mBACA,UACA,SACA,WACe;AACf,KAAI,CAAC,QACH;AAGF,KAAI;EACF,MAAM,QAAQ,MAAM,WAAW;AAC/B,QAAM,UACJ,KAAK,KAAK,mBAAmB,SAAS,EACtC,GAAG,KAAK,UAAU,OAAO,MAAM,EAAE,CAAC,KAClC,OACD;UACM,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,GAAG,SAAS,YAAY,EACrD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,eAAe,0BACb,mBACA,SACA,UACe;AACf,KAAI,CAAC,QACH;CAGF,MAAM,iBAAiB,KAAK,KAAK,mBAAmB,iBAAiB;AAErE,KAAI;AACF,QAAM,SAAS,IAAI,WAAW,eAAe;UACtC,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,uBAAuB,EACpD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,eAAe,oBACb,mBACA,UACA,SACA,WACe;AACf,KAAI,CAAC,QACH;AAGF,KAAI;AACF,QAAM,UAAU,KAAK,KAAK,mBAAmB,SAAS,EAAE,MAAM,WAAW,EAAE,OAAO;UAC3E,OAAO;AACd,QAAM,UACJ,KAAK,KAAK,mBAAmB,GAAG,SAAS,YAAY,EACrD,oBAAoB,MAAM,EAC1B,OACD;;;AAIL,SAAS,oBAAoB,OAAwB;AACnD,QAAO,iBAAiB,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO,MAAM,CAAC;;AAGzF,SAAS,gBAAgB,OAAuB;AAC9C,QACE,MACG,MAAM,CACN,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG,CACvB,MAAM,GAAG,GAAG,IAAI;;ACrMvB,SAAgB,mBACd,SACA,iBAAqC,EAAE,EACvC;CACA,MAAM,cACJ,eAAe,iBAAiB,aAA6B,eAAe,EAAE,UAAU,CAAC;AAE3F,QAAO;EAEL,UAAU,OACR,EAAE,cAAc,QAChB,QACG;GACH,MAAM,WAAW,qBAAqB,QAAQ;AAE9C,SAAM,SAAS,QAAQ;AACvB,4BAAyB;IAAE;IAAc;IAAM,EAAE,UAAU,QAAQ;AAEnE,OAAI;AACF,UAAM,IAAI,SAAS;aACX;AACR,UAAM,mBAAmB,SAAS,CAAC,YAAY;;;EAGnD,SAAS,OACP,EAAE,YACF,QACG;GACH,MAAM,UAAU,MAAM,iBAAiB;IACrC;IACA,aAAa,QAAQ,eAAA;IACrB,UAAU;IACX,CAAC;AAEF,OAAI;AACF,UAAM,IAAI,QAAQ;aACV;AACR,UAAM,QAAQ,SAAS;;;EAG3B,OAAO,OACL,EAAE,YACF,QACG;AACH,SAAM,IAAI,MAAM,YAAY,SAAS,CAAC;;EAEzC;;;;ACzDH,SAAgB,mBAAmB,SAAkD;AACnF,QAAOA,KAAK,OAAyB,mBAAmB,QAAQ,CAAC;;;;ACanE,SAAgB,iBAAiB,SAA8C;AAC7E,QAAOC,KAAK,OAAuB;EACjC,GAAG,mBAAmB,SAAS,EAC7B,MAAM,YAAY,UAAU;AAC1B,OAAI,QAAQ,UACV,OAAM,eAAe,UAAU,QAAQ,UAAU;AAGnD,UAAO,eAAe,EAAE,UAAU,CAAC;KAEtC,CAAC;EACF,QAAQ,OACN,EACE,UACA,cACA,QAEF,QACG;GACH,MAAM,SAAS,SAAS,OAAO,QAAQ,SAAS;GAChD,MAAM,aAAa,MAAM,OAAO,WAAW;AAE3C,OAAI,CAAC,WACH,OAAM,OAAO,OAAO,EAAE,QAAQ,QAAQ,cAAc,CAAC;AAGvD,OAAI,QAAQ,mBAAmB,KAAA,EAC7B,OAAM,OAAO,MAAM,CAAC,MAAM,QAAQ,eAAe;AAGnD,kCAA+B;IAAE;IAAc;IAAM,EAAE,QAAQ,QAAQ;AAEvE,OAAI;AACF,UAAM,IAAI,OAAO;aACT;AACR,QAAI,CAAC,WACH,OAAM,OAAO,QAAQ,EAAE,QAAQ,QAAQ,cAAc,CAAC;;;EAI7D,CAAC;;AAGJ,eAAe,eAAe,UAA0B,WAAqC;CAC3F,MAAM,YAAY,MAAM,SAAS,WAAW;AAE5C,MAAK,MAAM,CAAC,YAAY,UAAU,OAAO,QAAQ,UAAU,EAAE;EAC3D,MAAM,eAAe,KAAK,QAAQ,WAAW,GAAG,WAAW,MAAM,IAAI,CAAC,OAAO,QAAQ,CAAC;EACtF,MAAM,sBAAsB,KAAK,QAAQ,UAAU;AAEnD,MACE,iBAAiB,uBACjB,CAAC,aAAa,WAAW,GAAG,sBAAsB,KAAK,MAAM,CAE7D,OAAM,IAAI,MAAM,qCAAqC,aAAa;AAGpE,QAAM,mBAAmB,SAAS,CAAC,iBAAiB,aAAa;AACjE,QAAM,MAAM,KAAK,QAAQ,aAAa,EAAE,EAAE,WAAW,MAAM,CAAC;AAC5D,QAAM,eAAe,cAAc,MAAM;;;AAI7C,eAAe,eAAe,cAAsB,OAAsC;AACxF,KAAI,OAAO,UAAU,UAAU;AAC7B,QAAM,UAAU,cAAc,OAAO,OAAO;AAC5C;;AAGF,OAAM,UAAU,cAAc,GAAG,KAAK,UAAU,MAAM,MAAM,MAAM,EAAE,CAAC,KAAK,OAAO"}
|
|
1
|
+
{"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/vault-lock.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { TestContext } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions, FailureArtifactOptions } from \"./types\";\n\nconst DEFAULT_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\ninterface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport function getFailureArtifactConfig(\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: Pick<TestContext[\"task\"], \"id\" | \"name\">,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(\n artifactDirectory,\n \"editor.json\",\n config.capture.editorText,\n async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n }),\n ),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () =>\n obsidian.tabs(),\n ),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n ]);\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n });\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import { mkdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createHash, randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient } from \"../core/types\";\nimport type { SharedVaultLockOptions } from \"./types\";\n\nconst DEFAULT_HEARTBEAT_MS = 2_000;\nconst DEFAULT_STALE_MS = 15_000;\nconst DEFAULT_TIMEOUT_MS = 60_000;\nconst DEFAULT_WAIT_INTERVAL_MS = 500;\nconst DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), \"obsidian-e2e-locks\");\nconst LOCK_METADATA_FILE = \"lock.json\";\nconst APP_LOCK_KEY = \"__obsidianE2ELock\";\nconst heldLocks = new Map<string, HeldVaultRunLock>();\n\nexport interface VaultRunLockMetadata {\n acquiredAt: number;\n cwd: string;\n heartbeatAt: number;\n hostname: string;\n ownerId: string;\n pid: number;\n staleMs: number;\n vaultName: string;\n vaultPath: string;\n}\n\nexport interface VaultRunLock {\n readonly lockDir: string;\n readonly metadata: VaultRunLockMetadata;\n\n publishMarker(obsidian: ObsidianClient): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface VaultRunLockState {\n heartbeatAgeMs: number;\n isStale: boolean;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n}\n\ninterface AcquireVaultRunLockOptions extends SharedVaultLockOptions {\n vaultName: string;\n vaultPath: string;\n}\n\ninterface HeldVaultRunLock {\n heartbeat: NodeJS.Timeout;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n metadataPath: string;\n refs: number;\n}\n\nexport async function acquireVaultRunLock({\n heartbeatMs = DEFAULT_HEARTBEAT_MS,\n lockRoot = DEFAULT_LOCK_ROOT,\n onBusy = \"wait\",\n staleMs = DEFAULT_STALE_MS,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n vaultName,\n vaultPath,\n}: AcquireVaultRunLockOptions): Promise<VaultRunLock> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const heldLock = heldLocks.get(lockDir);\n\n if (heldLock) {\n heldLock.refs += 1;\n return createVaultRunLockHandle(heldLock);\n }\n\n const ownerId = randomUUID();\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n const metadata: VaultRunLockMetadata = {\n acquiredAt: Date.now(),\n cwd: process.cwd(),\n heartbeatAt: Date.now(),\n hostname: os.hostname(),\n ownerId,\n pid: process.pid,\n staleMs,\n vaultName,\n vaultPath,\n };\n\n await mkdir(lockRoot, { recursive: true });\n\n const startedAt = Date.now();\n\n while (true) {\n try {\n await mkdir(lockDir);\n await writeMetadata(metadataPath, metadata);\n break;\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const currentLock = await inspectVaultRunLock({\n lockRoot,\n staleMs,\n vaultPath,\n });\n\n if (currentLock && !currentLock.isStale) {\n if (onBusy === \"fail\") {\n throw new Error(formatBusyLockMessage(vaultPath, currentLock));\n }\n } else {\n await rm(lockDir, { force: true, recursive: true });\n continue;\n }\n\n if (Date.now() - startedAt >= timeoutMs) {\n throw new Error(\n currentLock\n ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}`\n : `Timed out waiting for shared vault lock on ${vaultPath}`,\n );\n }\n\n await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));\n }\n }\n\n const heartbeat = setInterval(() => {\n metadata.heartbeatAt = Date.now();\n void writeMetadata(metadataPath, metadata).catch(() => {});\n }, heartbeatMs);\n heartbeat.unref();\n\n const nextHeldLock: HeldVaultRunLock = {\n heartbeat,\n lockDir,\n metadata,\n metadataPath,\n refs: 1,\n };\n\n heldLocks.set(lockDir, nextHeldLock);\n return createVaultRunLockHandle(nextHeldLock);\n}\n\nexport async function clearVaultRunLockMarker(obsidian: ObsidianClient): Promise<void> {\n await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; \"cleared\"`, {\n allowNonZeroExit: true,\n });\n}\n\nexport async function inspectVaultRunLock({\n lockRoot = DEFAULT_LOCK_ROOT,\n staleMs = DEFAULT_STALE_MS,\n vaultPath,\n}: Pick<\n AcquireVaultRunLockOptions,\n \"lockRoot\" | \"staleMs\" | \"vaultPath\"\n>): Promise<VaultRunLockState | null> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const metadata = await readLockState(lockDir);\n\n if (!metadata) {\n return null;\n }\n\n return {\n heartbeatAgeMs: Date.now() - metadata.heartbeatAt,\n isStale: isLockStale(metadata, staleMs),\n lockDir,\n metadata,\n };\n}\n\nexport async function readVaultRunLockMarker(\n obsidian: ObsidianClient,\n): Promise<VaultRunLockMetadata | null> {\n return obsidian.dev.eval<VaultRunLockMetadata | null>(\n `window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`,\n { allowNonZeroExit: true },\n );\n}\n\nfunction createVaultLockKey(vaultPath: string): string {\n return createHash(\"sha256\").update(path.resolve(vaultPath)).digest(\"hex\");\n}\n\nfunction buildSetMarkerCode(metadata: VaultRunLockMetadata): string {\n const encodedMetadata = JSON.stringify(metadata);\n return `(() => {\n const lock = ${encodedMetadata};\n window.${APP_LOCK_KEY} = lock;\n app.${APP_LOCK_KEY} = lock;\n return lock;\n })()`;\n}\n\nfunction createVaultRunLockHandle(heldLock: HeldVaultRunLock): VaultRunLock {\n return {\n get lockDir() {\n return heldLock.lockDir;\n },\n get metadata() {\n return heldLock.metadata;\n },\n async publishMarker(obsidian: ObsidianClient) {\n await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));\n },\n async release() {\n if (heldLock.refs > 1) {\n heldLock.refs -= 1;\n return;\n }\n\n heldLocks.delete(heldLock.lockDir);\n clearInterval(heldLock.heartbeat);\n\n const currentLock = await readLockState(heldLock.lockDir);\n\n if (currentLock?.ownerId !== heldLock.metadata.ownerId) {\n return;\n }\n\n await rm(heldLock.lockDir, { force: true, recursive: true });\n },\n };\n}\n\nasync function readLockState(lockDir: string): Promise<VaultRunLockMetadata | null> {\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n\n try {\n return JSON.parse(await readFile(metadataPath, \"utf8\")) as VaultRunLockMetadata;\n } catch {\n try {\n const directoryStat = await stat(lockDir);\n return {\n acquiredAt: directoryStat.mtimeMs,\n cwd: \"\",\n heartbeatAt: directoryStat.mtimeMs,\n hostname: \"\",\n ownerId: \"\",\n pid: 0,\n staleMs: DEFAULT_STALE_MS,\n vaultName: \"\",\n vaultPath: \"\",\n };\n } catch {\n return null;\n }\n }\n}\n\nfunction formatBusyLockMessage(vaultPath: string, state: VaultRunLockState): string {\n const ownerDetails = state.metadata.ownerId\n ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || \"<unknown>\"}`\n : \"owner=<unknown>\";\n const ageDetails = `heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`;\n\n return `vault ${vaultPath} is locked by ${ownerDetails} ${ageDetails}`;\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isLockStale(metadata: VaultRunLockMetadata, staleMs: number): boolean {\n return Date.now() - metadata.heartbeatAt > staleMs;\n}\n\nasync function sleep(durationMs: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, durationMs));\n}\n\nasync function writeMetadata(metadataPath: string, metadata: VaultRunLockMetadata): Promise<void> {\n await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\\n`, \"utf8\");\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\nimport { acquireVaultRunLock, clearVaultRunLockMarker, type VaultRunLock } from \"./vault-lock\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\nexport interface BaseFixtureState {\n _vaultLock: VaultRunLock | null;\n}\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n _vaultLock: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use: (vaultLock: VaultRunLock | null) => Promise<void>) => {\n if (!options.sharedVaultLock) {\n await use(null);\n return;\n }\n\n const lockClient = createObsidianClient(options);\n await lockClient.verify();\n\n const lockOptions = options.sharedVaultLock === true ? {} : options.sharedVaultLock;\n const vaultLock = await acquireVaultRunLock({\n ...lockOptions,\n vaultName: options.vault,\n vaultPath: await lockClient.vaultPath(),\n });\n\n await vaultLock.publishMarker(lockClient);\n try {\n await use(vaultLock);\n } finally {\n try {\n await clearVaultRunLockMarker(lockClient);\n } catch {}\n\n await vaultLock.release();\n }\n },\n { scope: \"worker\" },\n ],\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n {\n _vaultLock,\n onTestFailed,\n task,\n }: Pick<BaseFixtureState & TestContext, \"_vaultLock\" | \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n if (_vaultLock) {\n await _vaultLock.publishMarker(obsidian);\n }\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures & BaseFixtureState>(\n createBaseFixtures(options) as never,\n ) as ObsidianTest;\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n const fixtures = {\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use: (plugin: PluginFixtures[\"plugin\"]) => Promise<void>,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n };\n\n return base.extend<PluginFixtures & BaseFixtureState>(fixtures as never) as PluginTest;\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vaultRoot = await obsidian.vaultPath();\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = path.resolve(vaultRoot, ...targetPath.split(\"/\").filter(Boolean));\n const normalizedVaultRoot = path.resolve(vaultRoot);\n\n if (\n resolvedPath !== normalizedVaultRoot &&\n !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)\n ) {\n throw new Error(`Seed path escapes the vault root: ${targetPath}`);\n }\n\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeSeedValue(resolvedPath, value);\n }\n}\n\nasync function writeSeedValue(resolvedPath: string, value: VaultSeedEntry): Promise<void> {\n if (typeof value === \"string\") {\n await writeFile(resolvedPath, value, \"utf8\");\n return;\n }\n\n await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Vitest-first end-to-end test utilities for Obsidian plugins.",
|
|
5
5
|
"homepage": "https://github.com/chhoumann/obsidian-e2e#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -35,11 +35,30 @@
|
|
|
35
35
|
},
|
|
36
36
|
"./package.json": "./package.json"
|
|
37
37
|
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@changesets/cli": "^2.30.0",
|
|
40
|
+
"@types/node": "^25.3.5",
|
|
41
|
+
"@typescript/native-preview": "7.0.0-dev.20260309.1",
|
|
42
|
+
"bumpp": "^10.4.1",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vite-plus": "latest",
|
|
45
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
46
|
+
},
|
|
38
47
|
"peerDependencies": {
|
|
39
48
|
"vite-plus": "^0.1.11"
|
|
40
49
|
},
|
|
41
50
|
"engines": {
|
|
42
51
|
"node": "^20.19.0 || >=22.12.0"
|
|
43
52
|
},
|
|
44
|
-
"
|
|
45
|
-
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "vp pack",
|
|
55
|
+
"check": "vp check",
|
|
56
|
+
"changeset": "changeset",
|
|
57
|
+
"dev": "vp pack --watch",
|
|
58
|
+
"release": "changeset publish",
|
|
59
|
+
"release:check": "vp check && vp test && vp pack",
|
|
60
|
+
"test": "vp test",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"version-packages": "changeset version"
|
|
63
|
+
}
|
|
64
|
+
}
|