obsidian-e2e 0.4.0 → 0.5.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 +163 -14
- package/dist/document-DunL2Moz.mjs +69 -0
- package/dist/document-DunL2Moz.mjs.map +1 -0
- package/dist/index.d.mts +16 -2
- package/dist/index.mjs +3 -2
- package/dist/matchers.d.mts +6 -0
- package/dist/matchers.mjs +30 -2
- package/dist/matchers.mjs.map +1 -1
- package/dist/{sandbox--mUbNsh7.mjs → test-context-BprSx6U1.mjs} +834 -194
- package/dist/test-context-BprSx6U1.mjs.map +1 -0
- package/dist/types-C4cj443K.d.mts +256 -0
- package/dist/vault-lock-CYyOdRP1.d.mts +136 -0
- package/dist/vitest.d.mts +1 -1
- package/dist/vitest.mjs +11 -88
- package/dist/vitest.mjs.map +1 -1
- package/package.json +4 -1
- package/dist/sandbox--mUbNsh7.mjs.map +0 -1
- package/dist/vault-lock-LmqAsLDT.d.mts +0 -282
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { n as parseNoteDocument, t as createNoteDocument } from "./document-DunL2Moz.mjs";
|
|
1
2
|
import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
2
3
|
import path, { posix } from "node:path";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
|
-
import os from "node:os";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
6
7
|
//#region src/core/args.ts
|
|
7
8
|
function buildCommandArgv(vaultName, command, args = {}) {
|
|
8
9
|
const argv = [`vault=${vaultName}`, command];
|
|
@@ -17,6 +18,303 @@ function buildCommandArgv(vaultName, command, args = {}) {
|
|
|
17
18
|
return argv;
|
|
18
19
|
}
|
|
19
20
|
//#endregion
|
|
21
|
+
//#region src/dev/harness.ts
|
|
22
|
+
const HARNESS_NAMESPACE = "__obsidianE2E";
|
|
23
|
+
const HARNESS_VERSION = 1;
|
|
24
|
+
function buildHarnessCallCode(method, ...args) {
|
|
25
|
+
return `(() => {
|
|
26
|
+
const __obsidianE2EMethod = ${JSON.stringify(method)};
|
|
27
|
+
const __obsidianE2EArgs = ${JSON.stringify(args)};
|
|
28
|
+
const __obsidianE2ENamespace = ${JSON.stringify(HARNESS_NAMESPACE)};
|
|
29
|
+
const __obsidianE2EVersion = ${HARNESS_VERSION};
|
|
30
|
+
${HARNESS_RUNTIME}
|
|
31
|
+
})()`;
|
|
32
|
+
}
|
|
33
|
+
function parseHarnessEnvelope(raw) {
|
|
34
|
+
const envelope = JSON.parse(raw.startsWith("=> ") ? raw.slice(3) : raw);
|
|
35
|
+
if (!envelope.ok) throw envelope.error;
|
|
36
|
+
return decodeHarnessValue(envelope.value);
|
|
37
|
+
}
|
|
38
|
+
function decodeHarnessValue(value) {
|
|
39
|
+
if (Array.isArray(value)) return value.map((entry) => decodeHarnessValue(entry));
|
|
40
|
+
if (!value || typeof value !== "object") return value;
|
|
41
|
+
if ("__obsidianE2EType" in value && value.__obsidianE2EType === "undefined") return;
|
|
42
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeHarnessValue(entry)]));
|
|
43
|
+
}
|
|
44
|
+
function createDevDiagnostics(value) {
|
|
45
|
+
return {
|
|
46
|
+
consoleMessages: value?.consoleMessages ?? [],
|
|
47
|
+
notices: value?.notices ?? [],
|
|
48
|
+
runtimeErrors: value?.runtimeErrors ?? []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const HARNESS_RUNTIME = String.raw`
|
|
52
|
+
const __obsidianE2EMaxEntries = 100;
|
|
53
|
+
|
|
54
|
+
const __obsidianE2EPush = (entries, value) => {
|
|
55
|
+
if (entries.length >= __obsidianE2EMaxEntries) {
|
|
56
|
+
entries.shift();
|
|
57
|
+
}
|
|
58
|
+
entries.push(value);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const __obsidianE2EFormat = (value) => {
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
} catch {
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const __obsidianE2ESerialize = (value, path = "$") => {
|
|
74
|
+
if (value === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return { __obsidianE2EType: "undefined" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const valueType = typeof value;
|
|
83
|
+
|
|
84
|
+
if (valueType === "string" || valueType === "boolean") {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (valueType === "number") {
|
|
89
|
+
if (!Number.isFinite(value)) {
|
|
90
|
+
throw new Error(\`Cannot serialize non-finite number at \${path}.\`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (valueType === "bigint" || valueType === "function" || valueType === "symbol") {
|
|
97
|
+
throw new Error(\`Cannot serialize \${valueType} at \${path}.\`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map((item, index) => __obsidianE2ESerialize(item, \`\${path}[\${index}]\`));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const prototype = Object.getPrototypeOf(value);
|
|
105
|
+
|
|
106
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
107
|
+
throw new Error(\`Cannot serialize non-plain object at \${path}.\`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const next = {};
|
|
111
|
+
|
|
112
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
113
|
+
next[key] = __obsidianE2ESerialize(entry, \`\${path}.\${key}\`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return next;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const __obsidianE2EClone = (value) => JSON.parse(JSON.stringify(__obsidianE2ESerialize(value)));
|
|
120
|
+
|
|
121
|
+
const __obsidianE2ECreateHarness = () => {
|
|
122
|
+
const state = {
|
|
123
|
+
consoleMessages: [],
|
|
124
|
+
notices: [],
|
|
125
|
+
runtimeErrors: [],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const pushConsoleMessage = (level, args) => {
|
|
129
|
+
__obsidianE2EPush(state.consoleMessages, {
|
|
130
|
+
args: args.map((entry) => {
|
|
131
|
+
try {
|
|
132
|
+
return __obsidianE2EClone(entry);
|
|
133
|
+
} catch {
|
|
134
|
+
return __obsidianE2EFormat(entry);
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
at: Date.now(),
|
|
138
|
+
level,
|
|
139
|
+
text: args.map(__obsidianE2EFormat).join(" "),
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const pushRuntimeError = (source, errorLike) => {
|
|
144
|
+
const message =
|
|
145
|
+
errorLike && typeof errorLike === "object" && "message" in errorLike
|
|
146
|
+
? String(errorLike.message)
|
|
147
|
+
: String(errorLike);
|
|
148
|
+
const stack =
|
|
149
|
+
errorLike && typeof errorLike === "object" && "stack" in errorLike
|
|
150
|
+
? String(errorLike.stack)
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
__obsidianE2EPush(state.runtimeErrors, {
|
|
154
|
+
at: Date.now(),
|
|
155
|
+
message,
|
|
156
|
+
source,
|
|
157
|
+
stack,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const installConsolePatch = (root) => {
|
|
162
|
+
if (root.__obsidianE2EConsolePatched) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const level of ["debug", "error", "info", "log", "warn"]) {
|
|
167
|
+
const original = root.console?.[level];
|
|
168
|
+
|
|
169
|
+
if (typeof original !== "function") {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
root.console[level] = (...args) => {
|
|
174
|
+
pushConsoleMessage(level, args);
|
|
175
|
+
return original.apply(root.console, args);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
root.__obsidianE2EConsolePatched = true;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const installRuntimePatch = (root) => {
|
|
183
|
+
if (root.__obsidianE2ERuntimePatched || typeof root.addEventListener !== "function") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
root.addEventListener("error", (event) => {
|
|
188
|
+
pushRuntimeError("error", event?.error ?? event?.message ?? "Unknown error");
|
|
189
|
+
});
|
|
190
|
+
root.addEventListener("unhandledrejection", (event) => {
|
|
191
|
+
pushRuntimeError("unhandledrejection", event?.reason ?? "Unhandled rejection");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
root.__obsidianE2ERuntimePatched = true;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const installNoticePatch = (root) => {
|
|
198
|
+
if (root.__obsidianE2ENoticePatched || typeof root.Notice !== "function") {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const OriginalNotice = root.Notice;
|
|
203
|
+
root.Notice = new Proxy(OriginalNotice, {
|
|
204
|
+
construct(target, ctorArgs, newTarget) {
|
|
205
|
+
__obsidianE2EPush(state.notices, {
|
|
206
|
+
at: Date.now(),
|
|
207
|
+
message: __obsidianE2EFormat(ctorArgs[0] ?? ""),
|
|
208
|
+
timeout:
|
|
209
|
+
typeof ctorArgs[1] === "number" && Number.isFinite(ctorArgs[1])
|
|
210
|
+
? ctorArgs[1]
|
|
211
|
+
: undefined,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return Reflect.construct(target, ctorArgs, newTarget);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
root.__obsidianE2ENoticePatched = true;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const ensureInstalled = () => {
|
|
221
|
+
const root = globalThis;
|
|
222
|
+
installConsolePatch(root);
|
|
223
|
+
installRuntimePatch(root);
|
|
224
|
+
installNoticePatch(root);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const getFileCache = (vaultPath) => {
|
|
228
|
+
const file = app?.vault?.getAbstractFileByPath?.(vaultPath);
|
|
229
|
+
if (!file) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return app?.metadataCache?.getFileCache?.(file) ?? null;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
activeFilePath() {
|
|
238
|
+
ensureInstalled();
|
|
239
|
+
return app?.workspace?.getActiveFile?.()?.path ?? null;
|
|
240
|
+
},
|
|
241
|
+
consoleMessages() {
|
|
242
|
+
ensureInstalled();
|
|
243
|
+
return state.consoleMessages;
|
|
244
|
+
},
|
|
245
|
+
diagnostics() {
|
|
246
|
+
ensureInstalled();
|
|
247
|
+
return state;
|
|
248
|
+
},
|
|
249
|
+
editorText() {
|
|
250
|
+
ensureInstalled();
|
|
251
|
+
return app?.workspace?.activeLeaf?.view?.editor?.getValue?.() ?? null;
|
|
252
|
+
},
|
|
253
|
+
eval(code) {
|
|
254
|
+
ensureInstalled();
|
|
255
|
+
return (0, eval)(code);
|
|
256
|
+
},
|
|
257
|
+
frontmatter(vaultPath) {
|
|
258
|
+
ensureInstalled();
|
|
259
|
+
return getFileCache(vaultPath)?.frontmatter ?? null;
|
|
260
|
+
},
|
|
261
|
+
metadata(vaultPath) {
|
|
262
|
+
ensureInstalled();
|
|
263
|
+
return getFileCache(vaultPath);
|
|
264
|
+
},
|
|
265
|
+
notices() {
|
|
266
|
+
ensureInstalled();
|
|
267
|
+
return state.notices;
|
|
268
|
+
},
|
|
269
|
+
pluginLoaded(pluginId) {
|
|
270
|
+
ensureInstalled();
|
|
271
|
+
const plugins = app?.plugins;
|
|
272
|
+
return Boolean(
|
|
273
|
+
plugins?.enabledPlugins?.has?.(pluginId) &&
|
|
274
|
+
plugins?.plugins?.[pluginId],
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
resetDiagnostics() {
|
|
278
|
+
state.consoleMessages.splice(0);
|
|
279
|
+
state.notices.splice(0);
|
|
280
|
+
state.runtimeErrors.splice(0);
|
|
281
|
+
ensureInstalled();
|
|
282
|
+
return true;
|
|
283
|
+
},
|
|
284
|
+
runtimeErrors() {
|
|
285
|
+
ensureInstalled();
|
|
286
|
+
return state.runtimeErrors;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const root = globalThis;
|
|
292
|
+
const current = root[__obsidianE2ENamespace];
|
|
293
|
+
const harness =
|
|
294
|
+
current && current.version === __obsidianE2EVersion
|
|
295
|
+
? current
|
|
296
|
+
: (root[__obsidianE2ENamespace] = {
|
|
297
|
+
api: __obsidianE2ECreateHarness(),
|
|
298
|
+
version: __obsidianE2EVersion,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const result = harness.api[__obsidianE2EMethod](...__obsidianE2EArgs);
|
|
303
|
+
return JSON.stringify({
|
|
304
|
+
ok: true,
|
|
305
|
+
value: __obsidianE2ESerialize(result),
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
error: {
|
|
310
|
+
message: error instanceof Error ? error.message : String(error),
|
|
311
|
+
name: error instanceof Error ? error.name : "Error",
|
|
312
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
313
|
+
},
|
|
314
|
+
ok: false,
|
|
315
|
+
});
|
|
316
|
+
}`;
|
|
317
|
+
//#endregion
|
|
20
318
|
//#region src/core/exec-options.ts
|
|
21
319
|
function mergeExecOptions(defaults, overrides) {
|
|
22
320
|
if (!defaults) return overrides ? { ...overrides } : {};
|
|
@@ -94,6 +392,40 @@ function isMissingFileError(error) {
|
|
|
94
392
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
95
393
|
}
|
|
96
394
|
//#endregion
|
|
395
|
+
//#region src/metadata/metadata.ts
|
|
396
|
+
function createObsidianMetadataHandle(client) {
|
|
397
|
+
return {
|
|
398
|
+
async fileCache(path, execOptions) {
|
|
399
|
+
return readMetadata(client, "metadata", path, execOptions);
|
|
400
|
+
},
|
|
401
|
+
async frontmatter(path, execOptions) {
|
|
402
|
+
return readMetadata(client, "frontmatter", path, execOptions);
|
|
403
|
+
},
|
|
404
|
+
async waitForFileCache(path, predicate, options) {
|
|
405
|
+
return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata cache", options);
|
|
406
|
+
},
|
|
407
|
+
async waitForFrontmatter(path, predicate, options) {
|
|
408
|
+
return waitForPresentValue(client, path, () => client.metadata.frontmatter(path), predicate, "frontmatter", options);
|
|
409
|
+
},
|
|
410
|
+
async waitForMetadata(path, predicate, options) {
|
|
411
|
+
return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata", options);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async function readMetadata(client, method, path, execOptions) {
|
|
416
|
+
return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode(method, path), execOptions));
|
|
417
|
+
}
|
|
418
|
+
async function waitForPresentValue(client, path, readValue, predicate, label, options = {}) {
|
|
419
|
+
return client.waitFor(async () => {
|
|
420
|
+
const value = await readValue();
|
|
421
|
+
if (value === null) return false;
|
|
422
|
+
return await (predicate?.(value) ?? true) ? value : false;
|
|
423
|
+
}, {
|
|
424
|
+
...options,
|
|
425
|
+
message: options.message ?? `vault path "${path}" to expose ${label}`
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
97
429
|
//#region src/vault/json-file.ts
|
|
98
430
|
function createJsonFile(filePath, beforeMutate) {
|
|
99
431
|
return {
|
|
@@ -125,17 +457,17 @@ function createPluginHandle(client, id) {
|
|
|
125
457
|
}
|
|
126
458
|
async function isLoadedInApp() {
|
|
127
459
|
try {
|
|
128
|
-
return await client.dev.
|
|
129
|
-
const plugins = app?.plugins;
|
|
130
|
-
return Boolean(
|
|
131
|
-
plugins?.enabledPlugins?.has?.(${JSON.stringify(id)}) &&
|
|
132
|
-
plugins?.plugins?.[${JSON.stringify(id)}],
|
|
133
|
-
);
|
|
134
|
-
})()`);
|
|
460
|
+
return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode("pluginLoaded", id)));
|
|
135
461
|
} catch {
|
|
136
462
|
return false;
|
|
137
463
|
}
|
|
138
464
|
}
|
|
465
|
+
function withDefaultReadyReloadOptions(options = {}) {
|
|
466
|
+
return {
|
|
467
|
+
...options,
|
|
468
|
+
waitUntilReady: options.waitUntilReady ?? true
|
|
469
|
+
};
|
|
470
|
+
}
|
|
139
471
|
return {
|
|
140
472
|
data() {
|
|
141
473
|
return {
|
|
@@ -180,6 +512,37 @@ function createPluginHandle(client, id) {
|
|
|
180
512
|
async restoreData() {
|
|
181
513
|
await getClientInternals(client).restoreFile(await resolveDataPath());
|
|
182
514
|
},
|
|
515
|
+
async updateDataAndReload(updater, options = {}) {
|
|
516
|
+
const nextData = await this.data().patch(updater);
|
|
517
|
+
if (await this.isEnabled()) await this.reload(withDefaultReadyReloadOptions(options));
|
|
518
|
+
return nextData;
|
|
519
|
+
},
|
|
520
|
+
async withPatchedData(updater, run, options = {}) {
|
|
521
|
+
const pluginWasEnabled = await this.isEnabled();
|
|
522
|
+
const reloadOptions = withDefaultReadyReloadOptions(options);
|
|
523
|
+
let hasPatchedData = false;
|
|
524
|
+
let runResult;
|
|
525
|
+
let runError;
|
|
526
|
+
let restoreError;
|
|
527
|
+
try {
|
|
528
|
+
await this.data().patch(updater);
|
|
529
|
+
hasPatchedData = true;
|
|
530
|
+
if (pluginWasEnabled) await this.reload(reloadOptions);
|
|
531
|
+
runResult = await run(this);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
runError = error;
|
|
534
|
+
}
|
|
535
|
+
if (hasPatchedData) try {
|
|
536
|
+
await this.restoreData();
|
|
537
|
+
if (pluginWasEnabled) await this.reload(reloadOptions);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
restoreError = error;
|
|
540
|
+
}
|
|
541
|
+
if (runError && restoreError) throw new AggregateError([runError, restoreError], `Plugin "${id}" patch execution and restore both failed.`);
|
|
542
|
+
if (runError) throw runError;
|
|
543
|
+
if (restoreError) throw restoreError;
|
|
544
|
+
return runResult;
|
|
545
|
+
},
|
|
183
546
|
async waitForData(predicate, options = {}) {
|
|
184
547
|
return client.waitFor(async () => {
|
|
185
548
|
try {
|
|
@@ -220,6 +583,15 @@ var WaitForTimeoutError = class extends Error {
|
|
|
220
583
|
this.causeError = causeError;
|
|
221
584
|
}
|
|
222
585
|
};
|
|
586
|
+
var DevEvalError = class extends Error {
|
|
587
|
+
remote;
|
|
588
|
+
constructor(message, remote) {
|
|
589
|
+
super(message);
|
|
590
|
+
this.name = "DevEvalError";
|
|
591
|
+
this.remote = remote;
|
|
592
|
+
if (remote.stack) this.stack = `${this.name}: ${message}\nRemote stack:\n${remote.stack}`;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
223
595
|
//#endregion
|
|
224
596
|
//#region src/core/transport.ts
|
|
225
597
|
const DEFAULT_TIMEOUT_MS$2 = 3e4;
|
|
@@ -303,6 +675,7 @@ function createObsidianClient(options) {
|
|
|
303
675
|
});
|
|
304
676
|
let cachedVaultPath;
|
|
305
677
|
const client = {};
|
|
678
|
+
const metadata = createObsidianMetadataHandle(client);
|
|
306
679
|
const app = {
|
|
307
680
|
async reload(execOptions = {}) {
|
|
308
681
|
await client.exec("reload", {}, execOptions);
|
|
@@ -326,32 +699,67 @@ function createObsidianClient(options) {
|
|
|
326
699
|
}, waitOptions);
|
|
327
700
|
}
|
|
328
701
|
};
|
|
702
|
+
const dev = {
|
|
703
|
+
async activeFilePath(execOptions = {}) {
|
|
704
|
+
return readHarnessValue(this, "activeFilePath", execOptions);
|
|
705
|
+
},
|
|
706
|
+
async consoleMessages(execOptions = {}) {
|
|
707
|
+
return readHarnessValue(this, "consoleMessages", execOptions);
|
|
708
|
+
},
|
|
709
|
+
async diagnostics(execOptions = {}) {
|
|
710
|
+
return createDevDiagnostics(await readHarnessValue(this, "diagnostics", execOptions));
|
|
711
|
+
},
|
|
712
|
+
async dom(options, execOptions = {}) {
|
|
713
|
+
const output = await client.execText("dev:dom", {
|
|
714
|
+
all: options.all,
|
|
715
|
+
attr: options.attr,
|
|
716
|
+
css: options.css,
|
|
717
|
+
inner: options.inner,
|
|
718
|
+
selector: options.selector,
|
|
719
|
+
text: options.text,
|
|
720
|
+
total: options.total
|
|
721
|
+
}, execOptions);
|
|
722
|
+
if (options.total) return Number.parseInt(output, 10);
|
|
723
|
+
if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
|
|
724
|
+
return output;
|
|
725
|
+
},
|
|
726
|
+
async eval(code, execOptions = {}) {
|
|
727
|
+
try {
|
|
728
|
+
return parseHarnessEnvelope(await this.evalRaw(buildHarnessCallCode("eval", code), execOptions));
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error && typeof error === "object" && "message" in error && "name" in error && typeof error.message === "string" && typeof error.name === "string") throw new DevEvalError(`Failed to evaluate Obsidian code: ${error.message}`, {
|
|
731
|
+
message: error.message,
|
|
732
|
+
name: error.name,
|
|
733
|
+
stack: "stack" in error && typeof error.stack === "string" ? error.stack : void 0
|
|
734
|
+
});
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
async evalRaw(code, execOptions = {}) {
|
|
739
|
+
return client.execText("eval", { code }, execOptions);
|
|
740
|
+
},
|
|
741
|
+
async editorText(execOptions = {}) {
|
|
742
|
+
return readHarnessValue(this, "editorText", execOptions);
|
|
743
|
+
},
|
|
744
|
+
async notices(execOptions = {}) {
|
|
745
|
+
return readHarnessValue(this, "notices", execOptions);
|
|
746
|
+
},
|
|
747
|
+
async resetDiagnostics(execOptions = {}) {
|
|
748
|
+
await readHarnessValue(this, "resetDiagnostics", execOptions);
|
|
749
|
+
},
|
|
750
|
+
async runtimeErrors(execOptions = {}) {
|
|
751
|
+
return readHarnessValue(this, "runtimeErrors", execOptions);
|
|
752
|
+
},
|
|
753
|
+
async screenshot(targetPath, execOptions = {}) {
|
|
754
|
+
await client.exec("dev:screenshot", { path: targetPath }, execOptions);
|
|
755
|
+
return targetPath;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
329
758
|
Object.assign(client, {
|
|
330
759
|
app,
|
|
331
760
|
bin: options.bin ?? "obsidian",
|
|
332
|
-
dev
|
|
333
|
-
|
|
334
|
-
const output = await client.execText("dev:dom", {
|
|
335
|
-
all: options.all,
|
|
336
|
-
attr: options.attr,
|
|
337
|
-
css: options.css,
|
|
338
|
-
inner: options.inner,
|
|
339
|
-
selector: options.selector,
|
|
340
|
-
text: options.text,
|
|
341
|
-
total: options.total
|
|
342
|
-
}, execOptions);
|
|
343
|
-
if (options.total) return Number.parseInt(output, 10);
|
|
344
|
-
if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
|
|
345
|
-
return output;
|
|
346
|
-
},
|
|
347
|
-
async eval(code, execOptions = {}) {
|
|
348
|
-
return parseDevEvalOutput(await client.execText("eval", { code }, execOptions));
|
|
349
|
-
},
|
|
350
|
-
async screenshot(targetPath, execOptions = {}) {
|
|
351
|
-
await client.exec("dev:screenshot", { path: targetPath }, execOptions);
|
|
352
|
-
return targetPath;
|
|
353
|
-
}
|
|
354
|
-
},
|
|
761
|
+
dev,
|
|
762
|
+
metadata,
|
|
355
763
|
command(id) {
|
|
356
764
|
return {
|
|
357
765
|
async exists(commandOptions = {}) {
|
|
@@ -419,6 +827,24 @@ function createObsidianClient(options) {
|
|
|
419
827
|
await this.vaultPath();
|
|
420
828
|
},
|
|
421
829
|
vaultName: options.vault,
|
|
830
|
+
async waitForActiveFile(path, options) {
|
|
831
|
+
return client.waitFor(async () => {
|
|
832
|
+
const activePath = await dev.activeFilePath();
|
|
833
|
+
return activePath === path ? activePath : false;
|
|
834
|
+
}, {
|
|
835
|
+
...options,
|
|
836
|
+
message: options?.message ?? `active file "${path}"`
|
|
837
|
+
});
|
|
838
|
+
},
|
|
839
|
+
async waitForConsoleMessage(predicate, options) {
|
|
840
|
+
return waitForDiagnosticEntry(client, () => client.dev.consoleMessages(), predicate, options?.message ?? "console message", options);
|
|
841
|
+
},
|
|
842
|
+
async waitForNotice(predicate, options) {
|
|
843
|
+
return waitForDiagnosticEntry(client, () => client.dev.notices(), typeof predicate === "string" ? (notice) => notice.message.includes(predicate) : predicate, options?.message ?? "notice", options);
|
|
844
|
+
},
|
|
845
|
+
async waitForRuntimeError(predicate, options) {
|
|
846
|
+
return waitForDiagnosticEntry(client, () => client.dev.runtimeErrors(), typeof predicate === "string" ? (error) => error.message.includes(predicate) : predicate, options?.message ?? "runtime error", options);
|
|
847
|
+
},
|
|
422
848
|
waitFor(fn, waitOptions) {
|
|
423
849
|
return waitForValue(fn, {
|
|
424
850
|
...waitDefaults,
|
|
@@ -435,14 +861,6 @@ function createObsidianClient(options) {
|
|
|
435
861
|
function parseCommandIds(output) {
|
|
436
862
|
return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
|
|
437
863
|
}
|
|
438
|
-
function parseDevEvalOutput(output) {
|
|
439
|
-
const normalized = output.startsWith("=> ") ? output.slice(3) : output;
|
|
440
|
-
try {
|
|
441
|
-
return JSON.parse(normalized);
|
|
442
|
-
} catch {
|
|
443
|
-
return normalized;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
864
|
function parseTabs(output) {
|
|
447
865
|
return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
|
|
448
866
|
}
|
|
@@ -478,6 +896,19 @@ function parseWorkspace(output) {
|
|
|
478
896
|
}
|
|
479
897
|
return roots;
|
|
480
898
|
}
|
|
899
|
+
async function waitForDiagnosticEntry(client, readEntries, predicate, label, options) {
|
|
900
|
+
return client.waitFor(async () => {
|
|
901
|
+
const entries = await readEntries();
|
|
902
|
+
for (const entry of entries) if (await predicate(entry)) return entry;
|
|
903
|
+
return false;
|
|
904
|
+
}, {
|
|
905
|
+
...options,
|
|
906
|
+
message: options?.message ?? label
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
async function readHarnessValue(dev, method, execOptions) {
|
|
910
|
+
return parseHarnessEnvelope(await dev.evalRaw(buildHarnessCallCode(method), execOptions));
|
|
911
|
+
}
|
|
481
912
|
function getWorkspaceDepth(line) {
|
|
482
913
|
let depth = 0;
|
|
483
914
|
let remainder = line;
|
|
@@ -518,51 +949,188 @@ function parseWorkspaceNode(line) {
|
|
|
518
949
|
};
|
|
519
950
|
}
|
|
520
951
|
//#endregion
|
|
952
|
+
//#region src/core/path-slug.ts
|
|
953
|
+
function sanitizePathSegment(value, options = {}) {
|
|
954
|
+
const fallback = options.fallback ?? "test";
|
|
955
|
+
const maxLength = options.maxLength ?? 80;
|
|
956
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength) || fallback;
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/vault/paths.ts
|
|
960
|
+
function normalizeScope(scope) {
|
|
961
|
+
if (!scope || scope === ".") return "";
|
|
962
|
+
return scope.replace(/^\/+|\/+$/g, "");
|
|
963
|
+
}
|
|
964
|
+
function resolveVaultPath(scopeRoot, targetPath) {
|
|
965
|
+
if (!targetPath || targetPath === ".") return scopeRoot;
|
|
966
|
+
return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
|
|
967
|
+
}
|
|
968
|
+
async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
|
|
969
|
+
const vaultPath = await obsidian.vaultPath();
|
|
970
|
+
const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
|
|
971
|
+
const resolvedPath = path.resolve(vaultPath, ...relativePath);
|
|
972
|
+
const normalizedVaultPath = path.resolve(vaultPath);
|
|
973
|
+
if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
|
|
974
|
+
return resolvedPath;
|
|
975
|
+
}
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/vault/vault.ts
|
|
978
|
+
function createVaultApi(options) {
|
|
979
|
+
const scopeRoot = normalizeScope(options.root);
|
|
980
|
+
return {
|
|
981
|
+
async delete(targetPath, deleteOptions = {}) {
|
|
982
|
+
await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
|
|
983
|
+
force: true,
|
|
984
|
+
recursive: true
|
|
985
|
+
});
|
|
986
|
+
if (deleteOptions.permanent === false) return;
|
|
987
|
+
},
|
|
988
|
+
async exists(targetPath) {
|
|
989
|
+
try {
|
|
990
|
+
await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
|
|
991
|
+
return true;
|
|
992
|
+
} catch {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
json(targetPath) {
|
|
997
|
+
const jsonFile = {
|
|
998
|
+
async patch(updater) {
|
|
999
|
+
const currentValue = await jsonFile.read();
|
|
1000
|
+
const draft = structuredClone(currentValue);
|
|
1001
|
+
const nextValue = await updater(draft) ?? draft;
|
|
1002
|
+
await jsonFile.write(nextValue);
|
|
1003
|
+
return nextValue;
|
|
1004
|
+
},
|
|
1005
|
+
async read() {
|
|
1006
|
+
const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
1007
|
+
return JSON.parse(rawValue);
|
|
1008
|
+
},
|
|
1009
|
+
async write(value) {
|
|
1010
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
1011
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1012
|
+
await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
return jsonFile;
|
|
1016
|
+
},
|
|
1017
|
+
async mkdir(targetPath) {
|
|
1018
|
+
await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
|
|
1019
|
+
},
|
|
1020
|
+
async read(targetPath) {
|
|
1021
|
+
return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
1022
|
+
},
|
|
1023
|
+
async waitForContent(targetPath, predicate, waitOptions = {}) {
|
|
1024
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
1025
|
+
return options.obsidian.waitFor(async () => {
|
|
1026
|
+
try {
|
|
1027
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
1028
|
+
return await predicate(content) ? content : false;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
}, {
|
|
1033
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
|
|
1034
|
+
...waitOptions
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
async waitForExists(targetPath, waitOptions) {
|
|
1038
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
1039
|
+
await options.obsidian.waitFor(async () => {
|
|
1040
|
+
try {
|
|
1041
|
+
await access(resolvedPath);
|
|
1042
|
+
return true;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
}, {
|
|
1047
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
|
|
1048
|
+
...waitOptions
|
|
1049
|
+
});
|
|
1050
|
+
},
|
|
1051
|
+
async waitForMissing(targetPath, waitOptions) {
|
|
1052
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
1053
|
+
await options.obsidian.waitFor(async () => {
|
|
1054
|
+
try {
|
|
1055
|
+
await access(resolvedPath);
|
|
1056
|
+
return false;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
}, {
|
|
1061
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
|
|
1062
|
+
...waitOptions
|
|
1063
|
+
});
|
|
1064
|
+
},
|
|
1065
|
+
async write(targetPath, content, writeOptions = {}) {
|
|
1066
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
1067
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
1068
|
+
await writeFile(resolvedPath, content, "utf8");
|
|
1069
|
+
if (!writeOptions.waitForContent) return;
|
|
1070
|
+
const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
|
|
1071
|
+
await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
521
1076
|
//#region src/artifacts/failure-artifacts.ts
|
|
522
1077
|
const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
|
|
1078
|
+
const DEFAULT_FAILURE_ARTIFACT_CAPTURE = {
|
|
1079
|
+
activeFile: true,
|
|
1080
|
+
activeNote: true,
|
|
1081
|
+
consoleMessages: true,
|
|
1082
|
+
dom: true,
|
|
1083
|
+
editorText: true,
|
|
1084
|
+
notices: true,
|
|
1085
|
+
parsedFrontmatter: true,
|
|
1086
|
+
runtimeErrors: true,
|
|
1087
|
+
screenshot: true,
|
|
1088
|
+
tabs: true,
|
|
1089
|
+
workspace: true
|
|
1090
|
+
};
|
|
523
1091
|
function getFailureArtifactConfig(options) {
|
|
524
1092
|
if (!options.captureOnFailure) return {
|
|
525
1093
|
artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
|
|
526
|
-
capture: {
|
|
527
|
-
activeFile: true,
|
|
528
|
-
dom: true,
|
|
529
|
-
editorText: true,
|
|
530
|
-
screenshot: true,
|
|
531
|
-
tabs: true,
|
|
532
|
-
workspace: true
|
|
533
|
-
},
|
|
1094
|
+
capture: { ...DEFAULT_FAILURE_ARTIFACT_CAPTURE },
|
|
534
1095
|
enabled: false
|
|
535
1096
|
};
|
|
536
1097
|
const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
|
|
537
1098
|
return {
|
|
538
1099
|
artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
|
|
539
1100
|
capture: {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
editorText: overrides.editorText ?? true,
|
|
543
|
-
screenshot: overrides.screenshot ?? true,
|
|
544
|
-
tabs: overrides.tabs ?? true,
|
|
545
|
-
workspace: overrides.workspace ?? true
|
|
1101
|
+
...DEFAULT_FAILURE_ARTIFACT_CAPTURE,
|
|
1102
|
+
...overrides
|
|
546
1103
|
},
|
|
547
1104
|
enabled: true
|
|
548
1105
|
};
|
|
549
1106
|
}
|
|
550
1107
|
function getFailureArtifactDirectory(artifactsDir, task) {
|
|
551
1108
|
const suffix = task.id.split("_").at(-1) ?? "test";
|
|
552
|
-
return path.join(artifactsDir, `${
|
|
1109
|
+
return path.join(artifactsDir, `${sanitizePathSegment(task.name, { maxLength: 60 })}-${suffix}`);
|
|
553
1110
|
}
|
|
554
1111
|
async function captureFailureArtifacts(task, obsidian, options) {
|
|
555
1112
|
const config = getFailureArtifactConfig(options);
|
|
556
1113
|
if (!config.enabled) return;
|
|
557
1114
|
const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
|
|
558
1115
|
await mkdir(artifactDirectory, { recursive: true });
|
|
1116
|
+
const activeFile = readArtifactInput(() => readActiveFilePath(obsidian));
|
|
1117
|
+
const activeNote = readArtifactInput(async () => {
|
|
1118
|
+
const activeFilePath = await unwrapArtifactInput(activeFile);
|
|
1119
|
+
return activeFilePath ? readActiveNoteSnapshot(obsidian, activeFilePath) : null;
|
|
1120
|
+
});
|
|
1121
|
+
const diagnostics = await obsidian.dev.diagnostics().catch(() => null);
|
|
559
1122
|
await Promise.all([
|
|
560
|
-
captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await
|
|
1123
|
+
captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await unwrapArtifactInput(activeFile) })),
|
|
1124
|
+
captureTextArtifact(artifactDirectory, "active-note.md", config.capture.activeNote, async () => (await unwrapArtifactInput(activeNote))?.raw ?? ""),
|
|
1125
|
+
captureJsonArtifact(artifactDirectory, "active-note-frontmatter.json", config.capture.parsedFrontmatter, async () => ({ frontmatter: (await unwrapArtifactInput(activeNote))?.frontmatter ?? null })),
|
|
561
1126
|
captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
|
|
562
1127
|
inner: true,
|
|
563
1128
|
selector: ".workspace"
|
|
564
1129
|
}))),
|
|
565
|
-
captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.
|
|
1130
|
+
captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.editorText() })),
|
|
1131
|
+
captureJsonArtifact(artifactDirectory, "console-messages.json", config.capture.consoleMessages, async () => diagnostics?.consoleMessages ?? []),
|
|
1132
|
+
captureJsonArtifact(artifactDirectory, "runtime-errors.json", config.capture.runtimeErrors, async () => diagnostics?.runtimeErrors ?? []),
|
|
1133
|
+
captureJsonArtifact(artifactDirectory, "notices.json", config.capture.notices, async () => diagnostics?.notices ?? []),
|
|
566
1134
|
captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
|
|
567
1135
|
captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
|
|
568
1136
|
captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
|
|
@@ -570,14 +1138,6 @@ async function captureFailureArtifacts(task, obsidian, options) {
|
|
|
570
1138
|
]);
|
|
571
1139
|
return artifactDirectory;
|
|
572
1140
|
}
|
|
573
|
-
async function capturePluginFailureArtifacts(task, plugin, options) {
|
|
574
|
-
const config = getFailureArtifactConfig(options);
|
|
575
|
-
if (!config.enabled) return;
|
|
576
|
-
const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
|
|
577
|
-
await mkdir(artifactDirectory, { recursive: true });
|
|
578
|
-
await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () => plugin.data().read());
|
|
579
|
-
return artifactDirectory;
|
|
580
|
-
}
|
|
581
1141
|
async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
|
|
582
1142
|
if (!enabled) return;
|
|
583
1143
|
try {
|
|
@@ -607,8 +1167,68 @@ async function captureTextArtifact(artifactDirectory, filename, enabled, readVal
|
|
|
607
1167
|
function formatArtifactError(error) {
|
|
608
1168
|
return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
|
|
609
1169
|
}
|
|
610
|
-
function
|
|
611
|
-
return
|
|
1170
|
+
async function readActiveFilePath(obsidian) {
|
|
1171
|
+
return obsidian.dev.activeFilePath();
|
|
1172
|
+
}
|
|
1173
|
+
async function readActiveNoteSnapshot(obsidian, activeFile) {
|
|
1174
|
+
return parseNoteDocument(await createVaultApi({ obsidian }).read(activeFile));
|
|
1175
|
+
}
|
|
1176
|
+
function readArtifactInput(readValue) {
|
|
1177
|
+
return { promise: readValue().then((value) => ({
|
|
1178
|
+
ok: true,
|
|
1179
|
+
value
|
|
1180
|
+
}), (error) => ({
|
|
1181
|
+
error,
|
|
1182
|
+
ok: false
|
|
1183
|
+
})) };
|
|
1184
|
+
}
|
|
1185
|
+
async function unwrapArtifactInput(result) {
|
|
1186
|
+
const value = await result.promise;
|
|
1187
|
+
if (!value.ok) throw value.error;
|
|
1188
|
+
return value.value;
|
|
1189
|
+
}
|
|
1190
|
+
//#endregion
|
|
1191
|
+
//#region src/vault/sandbox.ts
|
|
1192
|
+
async function createSandboxApi(options) {
|
|
1193
|
+
const root = posix.join(options.sandboxRoot, `${sanitizePathSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
|
|
1194
|
+
const sandboxPath = (...segments) => posix.join(root, ...segments);
|
|
1195
|
+
const vault = createVaultApi({
|
|
1196
|
+
obsidian: options.obsidian,
|
|
1197
|
+
root
|
|
1198
|
+
});
|
|
1199
|
+
await vault.mkdir(".");
|
|
1200
|
+
return {
|
|
1201
|
+
...vault,
|
|
1202
|
+
async cleanup() {
|
|
1203
|
+
await vault.delete(".", { permanent: true });
|
|
1204
|
+
},
|
|
1205
|
+
async frontmatter(targetPath) {
|
|
1206
|
+
return options.obsidian.metadata.frontmatter(sandboxPath(targetPath));
|
|
1207
|
+
},
|
|
1208
|
+
path(...segments) {
|
|
1209
|
+
return sandboxPath(...segments);
|
|
1210
|
+
},
|
|
1211
|
+
async readNote(targetPath) {
|
|
1212
|
+
return parseNoteDocument(await vault.read(targetPath));
|
|
1213
|
+
},
|
|
1214
|
+
root,
|
|
1215
|
+
async waitForFrontmatter(targetPath, predicate, waitOptions) {
|
|
1216
|
+
return options.obsidian.metadata.waitForFrontmatter(sandboxPath(targetPath), predicate, waitOptions);
|
|
1217
|
+
},
|
|
1218
|
+
async waitForMetadata(targetPath, predicate, waitOptions) {
|
|
1219
|
+
return options.obsidian.metadata.waitForMetadata(sandboxPath(targetPath), predicate, waitOptions);
|
|
1220
|
+
},
|
|
1221
|
+
async writeNote(writeOptions) {
|
|
1222
|
+
const { path, waitForMetadata = true, waitOptions, ...noteInput } = writeOptions;
|
|
1223
|
+
const document = createNoteDocument(noteInput);
|
|
1224
|
+
await vault.write(path, document.raw);
|
|
1225
|
+
if (waitForMetadata) {
|
|
1226
|
+
const predicate = typeof waitForMetadata === "function" ? waitForMetadata : void 0;
|
|
1227
|
+
await options.obsidian.metadata.waitForMetadata(sandboxPath(path), predicate, waitOptions);
|
|
1228
|
+
}
|
|
1229
|
+
return document;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
612
1232
|
}
|
|
613
1233
|
//#endregion
|
|
614
1234
|
//#region src/fixtures/vault-lock.ts
|
|
@@ -772,147 +1392,167 @@ async function sleep(durationMs) {
|
|
|
772
1392
|
async function writeMetadata(metadataPath, metadata) {
|
|
773
1393
|
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
774
1394
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
function normalizeScope(scope) {
|
|
778
|
-
if (!scope || scope === ".") return "";
|
|
779
|
-
return scope.replace(/^\/+|\/+$/g, "");
|
|
780
|
-
}
|
|
781
|
-
function resolveVaultPath(scopeRoot, targetPath) {
|
|
782
|
-
if (!targetPath || targetPath === ".") return scopeRoot;
|
|
783
|
-
return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
|
|
784
|
-
}
|
|
785
|
-
async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
|
|
786
|
-
const vaultPath = await obsidian.vaultPath();
|
|
787
|
-
const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
|
|
788
|
-
const resolvedPath = path.resolve(vaultPath, ...relativePath);
|
|
789
|
-
const normalizedVaultPath = path.resolve(vaultPath);
|
|
790
|
-
if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
|
|
791
|
-
return resolvedPath;
|
|
792
|
-
}
|
|
793
|
-
//#endregion
|
|
794
|
-
//#region src/vault/vault.ts
|
|
795
|
-
function createVaultApi(options) {
|
|
796
|
-
const scopeRoot = normalizeScope(options.root);
|
|
1395
|
+
function createBaseFixtures(options, fixtureOptions = {}) {
|
|
1396
|
+
const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
|
|
797
1397
|
return {
|
|
798
|
-
async
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1398
|
+
_vaultLock: [async ({}, use) => {
|
|
1399
|
+
if (!options.sharedVaultLock) {
|
|
1400
|
+
await use(null);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const lockClient = createObsidianClient(options);
|
|
1404
|
+
await lockClient.verify();
|
|
1405
|
+
const vaultLock = await acquireVaultRunLock({
|
|
1406
|
+
...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
|
|
1407
|
+
vaultName: options.vault,
|
|
1408
|
+
vaultPath: await lockClient.vaultPath()
|
|
802
1409
|
});
|
|
803
|
-
|
|
804
|
-
},
|
|
805
|
-
async exists(targetPath) {
|
|
1410
|
+
await vaultLock.publishMarker(lockClient);
|
|
806
1411
|
try {
|
|
807
|
-
await
|
|
808
|
-
|
|
809
|
-
} catch {
|
|
810
|
-
return false;
|
|
811
|
-
}
|
|
812
|
-
},
|
|
813
|
-
json(targetPath) {
|
|
814
|
-
const jsonFile = {
|
|
815
|
-
async patch(updater) {
|
|
816
|
-
const currentValue = await jsonFile.read();
|
|
817
|
-
const draft = structuredClone(currentValue);
|
|
818
|
-
const nextValue = await updater(draft) ?? draft;
|
|
819
|
-
await jsonFile.write(nextValue);
|
|
820
|
-
return nextValue;
|
|
821
|
-
},
|
|
822
|
-
async read() {
|
|
823
|
-
const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
824
|
-
return JSON.parse(rawValue);
|
|
825
|
-
},
|
|
826
|
-
async write(value) {
|
|
827
|
-
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
828
|
-
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
829
|
-
await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
|
-
return jsonFile;
|
|
833
|
-
},
|
|
834
|
-
async mkdir(targetPath) {
|
|
835
|
-
await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
|
|
836
|
-
},
|
|
837
|
-
async read(targetPath) {
|
|
838
|
-
return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
839
|
-
},
|
|
840
|
-
async waitForContent(targetPath, predicate, waitOptions = {}) {
|
|
841
|
-
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
842
|
-
return options.obsidian.waitFor(async () => {
|
|
1412
|
+
await use(vaultLock);
|
|
1413
|
+
} finally {
|
|
843
1414
|
try {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1415
|
+
await clearVaultRunLockMarker(lockClient);
|
|
1416
|
+
} catch {}
|
|
1417
|
+
await vaultLock.release();
|
|
1418
|
+
}
|
|
1419
|
+
}, { scope: "worker" }],
|
|
1420
|
+
_testContext: async ({ _vaultLock, onTestFailed, task }, use) => {
|
|
1421
|
+
let failedTask = false;
|
|
1422
|
+
onTestFailed(() => {
|
|
1423
|
+
failedTask = true;
|
|
852
1424
|
});
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
await access(resolvedPath);
|
|
859
|
-
return true;
|
|
860
|
-
} catch {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
}, {
|
|
864
|
-
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
|
|
865
|
-
...waitOptions
|
|
1425
|
+
const context = await createInternalTestContext({
|
|
1426
|
+
...options,
|
|
1427
|
+
createVault,
|
|
1428
|
+
testName: task.name,
|
|
1429
|
+
vaultLock: _vaultLock
|
|
866
1430
|
});
|
|
1431
|
+
try {
|
|
1432
|
+
await use(context);
|
|
1433
|
+
} finally {
|
|
1434
|
+
await context.cleanup({ failedTask: failedTask ? task : void 0 });
|
|
1435
|
+
}
|
|
867
1436
|
},
|
|
868
|
-
async
|
|
869
|
-
|
|
870
|
-
await options.obsidian.waitFor(async () => {
|
|
871
|
-
try {
|
|
872
|
-
await access(resolvedPath);
|
|
873
|
-
return false;
|
|
874
|
-
} catch {
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
}, {
|
|
878
|
-
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
|
|
879
|
-
...waitOptions
|
|
880
|
-
});
|
|
1437
|
+
obsidian: async ({ _testContext }, use) => {
|
|
1438
|
+
await use(_testContext.obsidian);
|
|
881
1439
|
},
|
|
882
|
-
async
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
|
|
888
|
-
await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
|
|
1440
|
+
sandbox: async ({ _testContext }, use) => {
|
|
1441
|
+
await use(_testContext.sandbox);
|
|
1442
|
+
},
|
|
1443
|
+
vault: async ({ _testContext }, use) => {
|
|
1444
|
+
await use(_testContext.vault);
|
|
889
1445
|
}
|
|
890
1446
|
};
|
|
891
1447
|
}
|
|
892
1448
|
//#endregion
|
|
893
|
-
//#region src/
|
|
894
|
-
async function
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1449
|
+
//#region src/fixtures/test-context.ts
|
|
1450
|
+
async function createTestContext(options) {
|
|
1451
|
+
return createInternalTestContext(options);
|
|
1452
|
+
}
|
|
1453
|
+
async function withVaultSandbox(options, run) {
|
|
1454
|
+
const context = await createInternalTestContext(options);
|
|
1455
|
+
try {
|
|
1456
|
+
return await run(context);
|
|
1457
|
+
} finally {
|
|
1458
|
+
await context.cleanup();
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async function createInternalTestContext(options) {
|
|
1462
|
+
const obsidian = createObsidianClient(options);
|
|
1463
|
+
const trackedPlugins = /* @__PURE__ */ new Map();
|
|
1464
|
+
let sandbox = null;
|
|
1465
|
+
const vaultLock = options.vaultLock ?? await maybeAcquireVaultLock(options, obsidian);
|
|
1466
|
+
const ownsVaultLock = options.vaultLock === void 0 && vaultLock !== null;
|
|
1467
|
+
const vaultFactory = options.createVault ?? ((client) => createVaultApi({ obsidian: client }));
|
|
1468
|
+
let disposed = false;
|
|
1469
|
+
try {
|
|
1470
|
+
await obsidian.verify();
|
|
1471
|
+
if (vaultLock) await vaultLock.publishMarker(obsidian);
|
|
1472
|
+
await obsidian.dev.resetDiagnostics().catch(() => {});
|
|
1473
|
+
const vault = await vaultFactory(obsidian);
|
|
1474
|
+
sandbox = await createSandboxApi({
|
|
1475
|
+
obsidian,
|
|
1476
|
+
sandboxRoot: options.sandboxRoot ?? "__obsidian_e2e__",
|
|
1477
|
+
testName: options.testName ?? "test"
|
|
1478
|
+
});
|
|
1479
|
+
const captureArtifacts = async (task) => captureFailureArtifacts(task, obsidian, {
|
|
1480
|
+
...options,
|
|
1481
|
+
plugin: trackedPlugins.size === 1 ? [...trackedPlugins.values()][0].plugin : void 0
|
|
1482
|
+
});
|
|
1483
|
+
const cleanup = async (cleanupOptions = {}) => {
|
|
1484
|
+
if (disposed) return;
|
|
1485
|
+
disposed = true;
|
|
1486
|
+
const cleanupErrors = [];
|
|
1487
|
+
try {
|
|
1488
|
+
if (cleanupOptions.failedTask && options.captureOnFailure) await captureArtifacts(cleanupOptions.failedTask);
|
|
1489
|
+
} finally {
|
|
1490
|
+
try {
|
|
1491
|
+
await getClientInternals(obsidian).restoreAll();
|
|
1492
|
+
} finally {
|
|
1493
|
+
for (const session of [...trackedPlugins.values()].reverse()) if (!session.wasEnabled) await recordCleanupError(cleanupErrors, async () => session.plugin.disable({ filter: session.filter }));
|
|
1494
|
+
try {
|
|
1495
|
+
await clearVaultRunLockMarker(obsidian);
|
|
1496
|
+
} catch {}
|
|
1497
|
+
if (ownsVaultLock) await recordCleanupError(cleanupErrors, async () => vaultLock.release());
|
|
1498
|
+
await recordCleanupError(cleanupErrors, async () => sandbox.cleanup());
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
|
1502
|
+
if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "One or more test cleanup steps failed.");
|
|
1503
|
+
};
|
|
1504
|
+
return {
|
|
1505
|
+
obsidian,
|
|
1506
|
+
sandbox,
|
|
1507
|
+
vault,
|
|
1508
|
+
captureFailureArtifacts: captureArtifacts,
|
|
1509
|
+
cleanup,
|
|
1510
|
+
async plugin(id, sessionOptions = {}) {
|
|
1511
|
+
const existing = trackedPlugins.get(id);
|
|
1512
|
+
if (existing) return existing.plugin;
|
|
1513
|
+
const plugin = obsidian.plugin(id);
|
|
1514
|
+
const wasEnabled = await plugin.isEnabled();
|
|
1515
|
+
if (!wasEnabled) await plugin.enable({ filter: sessionOptions.filter });
|
|
1516
|
+
if (sessionOptions.seedData !== void 0) await plugin.data().write(sessionOptions.seedData);
|
|
1517
|
+
trackedPlugins.set(id, {
|
|
1518
|
+
filter: sessionOptions.filter,
|
|
1519
|
+
plugin,
|
|
1520
|
+
wasEnabled
|
|
1521
|
+
});
|
|
1522
|
+
return plugin;
|
|
1523
|
+
},
|
|
1524
|
+
async resetDiagnostics() {
|
|
1525
|
+
await obsidian.dev.resetDiagnostics().catch(() => {});
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
try {
|
|
1530
|
+
if (sandbox) await sandbox.cleanup();
|
|
1531
|
+
} finally {
|
|
1532
|
+
try {
|
|
1533
|
+
await clearVaultRunLockMarker(obsidian);
|
|
1534
|
+
} catch {}
|
|
1535
|
+
if (ownsVaultLock) await vaultLock.release();
|
|
1536
|
+
}
|
|
1537
|
+
throw error;
|
|
1538
|
+
}
|
|
911
1539
|
}
|
|
912
|
-
function
|
|
913
|
-
|
|
1540
|
+
async function recordCleanupError(cleanupErrors, run) {
|
|
1541
|
+
try {
|
|
1542
|
+
await run();
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
cleanupErrors.push(error);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
async function maybeAcquireVaultLock(options, obsidian) {
|
|
1548
|
+
if (!options.sharedVaultLock) return null;
|
|
1549
|
+
return acquireVaultRunLock({
|
|
1550
|
+
...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
|
|
1551
|
+
vaultName: options.vault,
|
|
1552
|
+
vaultPath: await obsidian.vaultPath()
|
|
1553
|
+
});
|
|
914
1554
|
}
|
|
915
1555
|
//#endregion
|
|
916
|
-
export { clearVaultRunLockMarker as a,
|
|
1556
|
+
export { clearVaultRunLockMarker as a, createSandboxApi as c, createVaultApi as d, resolveFilesystemPath as f, acquireVaultRunLock as i, DEFAULT_FAILURE_ARTIFACTS_DIR as l, getClientInternals as m, withVaultSandbox as n, inspectVaultRunLock as o, createObsidianClient as p, createBaseFixtures as r, readVaultRunLockMarker as s, createTestContext as t, captureFailureArtifacts as u };
|
|
917
1557
|
|
|
918
|
-
//# sourceMappingURL=
|
|
1558
|
+
//# sourceMappingURL=test-context-BprSx6U1.mjs.map
|