obsidian-e2e 0.4.0 → 0.6.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 +166 -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 +34 -2
- package/dist/matchers.mjs.map +1 -1
- package/dist/{sandbox--mUbNsh7.mjs → test-context-Bl-e-83H.mjs} +647 -212
- package/dist/test-context-Bl-e-83H.mjs.map +1 -0
- package/dist/{vault-lock-LmqAsLDT.d.mts → types-BUXaueDI.d.mts} +84 -112
- package/dist/vault-lock-XcBHtwm9.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
|
@@ -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];
|
|
@@ -94,6 +95,133 @@ function isMissingFileError(error) {
|
|
|
94
95
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
95
96
|
}
|
|
96
97
|
//#endregion
|
|
98
|
+
//#region src/core/errors.ts
|
|
99
|
+
var ObsidianCommandError = class extends Error {
|
|
100
|
+
result;
|
|
101
|
+
constructor(message, result) {
|
|
102
|
+
super(message);
|
|
103
|
+
this.name = "ObsidianCommandError";
|
|
104
|
+
this.result = result;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var WaitForTimeoutError = class extends Error {
|
|
108
|
+
causeError;
|
|
109
|
+
constructor(message, causeError) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "WaitForTimeoutError";
|
|
112
|
+
this.causeError = causeError;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var DevEvalError = class extends Error {
|
|
116
|
+
remote;
|
|
117
|
+
constructor(message, remote) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = "DevEvalError";
|
|
120
|
+
this.remote = remote;
|
|
121
|
+
if (remote.stack) this.stack = `${this.name}: ${message}\nRemote stack:\n${remote.stack}`;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/dev/eval-json.ts
|
|
126
|
+
async function runEvalJson(dev, code, execOptions = {}) {
|
|
127
|
+
return parseEvalJsonEnvelope(await dev.evalRaw(buildEvalJsonCode(code), execOptions));
|
|
128
|
+
}
|
|
129
|
+
function buildEvalJsonCode(code) {
|
|
130
|
+
return [
|
|
131
|
+
"(()=>{",
|
|
132
|
+
`const __obsidianE2ECode=${JSON.stringify(code)};`,
|
|
133
|
+
"const __obsidianE2ESerialize=(value,path='$')=>{",
|
|
134
|
+
"if(value===null){return null;}",
|
|
135
|
+
"if(value===undefined){return {__obsidianE2EType:'undefined'};}",
|
|
136
|
+
"const valueType=typeof value;",
|
|
137
|
+
"if(valueType==='string'||valueType==='boolean'){return value;}",
|
|
138
|
+
"if(valueType==='number'){if(!Number.isFinite(value)){throw new Error(`Cannot serialize non-finite number at ${path}.`);}return value;}",
|
|
139
|
+
"if(valueType==='bigint'||valueType==='function'||valueType==='symbol'){throw new Error(`Cannot serialize ${valueType} at ${path}.`);}",
|
|
140
|
+
"if(Array.isArray(value)){return value.map((item,index)=>__obsidianE2ESerialize(item,`${path}[${index}]`));}",
|
|
141
|
+
"const prototype=Object.getPrototypeOf(value);",
|
|
142
|
+
"if(prototype!==Object.prototype&&prototype!==null){throw new Error(`Cannot serialize non-plain object at ${path}.`);}",
|
|
143
|
+
"const next={};",
|
|
144
|
+
"for(const [key,entry] of Object.entries(value)){next[key]=__obsidianE2ESerialize(entry,`${path}.${key}`);}",
|
|
145
|
+
"return next;",
|
|
146
|
+
"};",
|
|
147
|
+
"try{",
|
|
148
|
+
"return JSON.stringify({ok:true,value:__obsidianE2ESerialize((0,eval)(__obsidianE2ECode))});",
|
|
149
|
+
"}catch(error){",
|
|
150
|
+
"return JSON.stringify({ok:false,error:{message:error instanceof Error?error.message:String(error),name:error instanceof Error?error.name:'Error',stack:error instanceof Error?error.stack:undefined}});",
|
|
151
|
+
"}",
|
|
152
|
+
"})()"
|
|
153
|
+
].join("");
|
|
154
|
+
}
|
|
155
|
+
function parseDevEvalOutput(raw) {
|
|
156
|
+
const normalized = normalizeEvalOutput(raw);
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(normalized);
|
|
159
|
+
} catch {
|
|
160
|
+
return normalized;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function parseEvalJsonEnvelope(raw) {
|
|
164
|
+
const envelope = JSON.parse(normalizeEvalOutput(raw));
|
|
165
|
+
if (!envelope.ok) throw new DevEvalError(`Failed to evaluate Obsidian code: ${envelope.error.message}`, { ...envelope.error });
|
|
166
|
+
return decodeEvalJsonValue(envelope.value);
|
|
167
|
+
}
|
|
168
|
+
function normalizeEvalOutput(raw) {
|
|
169
|
+
return raw.startsWith("=> ") ? raw.slice(3) : raw;
|
|
170
|
+
}
|
|
171
|
+
function decodeEvalJsonValue(value) {
|
|
172
|
+
if (Array.isArray(value)) return value.map((entry) => decodeEvalJsonValue(entry));
|
|
173
|
+
if (!value || typeof value !== "object") return value;
|
|
174
|
+
if (isUndefinedSentinel(value)) return;
|
|
175
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeEvalJsonValue(entry)]));
|
|
176
|
+
}
|
|
177
|
+
function isUndefinedSentinel(value) {
|
|
178
|
+
return "__obsidianE2EType" in value && value.__obsidianE2EType === "undefined";
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/metadata/metadata.ts
|
|
182
|
+
function createObsidianMetadataHandle(client) {
|
|
183
|
+
return {
|
|
184
|
+
async fileCache(path, execOptions) {
|
|
185
|
+
return readMetadata(client, "metadata", path, execOptions);
|
|
186
|
+
},
|
|
187
|
+
async frontmatter(path, execOptions) {
|
|
188
|
+
return readMetadata(client, "frontmatter", path, execOptions);
|
|
189
|
+
},
|
|
190
|
+
async waitForFileCache(path, predicate, options) {
|
|
191
|
+
return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata cache", options);
|
|
192
|
+
},
|
|
193
|
+
async waitForFrontmatter(path, predicate, options) {
|
|
194
|
+
return waitForPresentValue(client, path, () => client.metadata.frontmatter(path), predicate, "frontmatter", options);
|
|
195
|
+
},
|
|
196
|
+
async waitForMetadata(path, predicate, options) {
|
|
197
|
+
return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata", options);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function readMetadata(client, method, path, execOptions) {
|
|
202
|
+
return runEvalJson(client.dev, buildMetadataReadCode(method, path), execOptions);
|
|
203
|
+
}
|
|
204
|
+
function buildMetadataReadCode(method, path) {
|
|
205
|
+
return [
|
|
206
|
+
"(()=>{",
|
|
207
|
+
`const __obsidianE2EPath=${JSON.stringify(path)};`,
|
|
208
|
+
"const __obsidianE2EFile=app?.vault?.getAbstractFileByPath?.(__obsidianE2EPath);",
|
|
209
|
+
"if(!__obsidianE2EFile){return null;}",
|
|
210
|
+
method === "frontmatter" ? "return app?.metadataCache?.getFileCache?.(__obsidianE2EFile)?.frontmatter ?? null;" : "return app?.metadataCache?.getFileCache?.(__obsidianE2EFile) ?? null;",
|
|
211
|
+
"})()"
|
|
212
|
+
].join("");
|
|
213
|
+
}
|
|
214
|
+
async function waitForPresentValue(client, path, readValue, predicate, label, options = {}) {
|
|
215
|
+
return client.waitFor(async () => {
|
|
216
|
+
const value = await readValue();
|
|
217
|
+
if (value === null) return false;
|
|
218
|
+
return await (predicate?.(value) ?? true) ? value : false;
|
|
219
|
+
}, {
|
|
220
|
+
...options,
|
|
221
|
+
message: options.message ?? `vault path "${path}" to expose ${label}`
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
97
225
|
//#region src/vault/json-file.ts
|
|
98
226
|
function createJsonFile(filePath, beforeMutate) {
|
|
99
227
|
return {
|
|
@@ -125,17 +253,17 @@ function createPluginHandle(client, id) {
|
|
|
125
253
|
}
|
|
126
254
|
async function isLoadedInApp() {
|
|
127
255
|
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
|
-
})()`);
|
|
256
|
+
return await runEvalJson(client.dev, buildPluginLoadedCode(id));
|
|
135
257
|
} catch {
|
|
136
258
|
return false;
|
|
137
259
|
}
|
|
138
260
|
}
|
|
261
|
+
function withDefaultReadyReloadOptions(options = {}) {
|
|
262
|
+
return {
|
|
263
|
+
...options,
|
|
264
|
+
waitUntilReady: options.waitUntilReady ?? true
|
|
265
|
+
};
|
|
266
|
+
}
|
|
139
267
|
return {
|
|
140
268
|
data() {
|
|
141
269
|
return {
|
|
@@ -180,6 +308,37 @@ function createPluginHandle(client, id) {
|
|
|
180
308
|
async restoreData() {
|
|
181
309
|
await getClientInternals(client).restoreFile(await resolveDataPath());
|
|
182
310
|
},
|
|
311
|
+
async updateDataAndReload(updater, options = {}) {
|
|
312
|
+
const nextData = await this.data().patch(updater);
|
|
313
|
+
if (await this.isEnabled()) await this.reload(withDefaultReadyReloadOptions(options));
|
|
314
|
+
return nextData;
|
|
315
|
+
},
|
|
316
|
+
async withPatchedData(updater, run, options = {}) {
|
|
317
|
+
const pluginWasEnabled = await this.isEnabled();
|
|
318
|
+
const reloadOptions = withDefaultReadyReloadOptions(options);
|
|
319
|
+
let hasPatchedData = false;
|
|
320
|
+
let runResult;
|
|
321
|
+
let runError;
|
|
322
|
+
let restoreError;
|
|
323
|
+
try {
|
|
324
|
+
await this.data().patch(updater);
|
|
325
|
+
hasPatchedData = true;
|
|
326
|
+
if (pluginWasEnabled) await this.reload(reloadOptions);
|
|
327
|
+
runResult = await run(this);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
runError = error;
|
|
330
|
+
}
|
|
331
|
+
if (hasPatchedData) try {
|
|
332
|
+
await this.restoreData();
|
|
333
|
+
if (pluginWasEnabled) await this.reload(reloadOptions);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
restoreError = error;
|
|
336
|
+
}
|
|
337
|
+
if (runError && restoreError) throw new AggregateError([runError, restoreError], `Plugin "${id}" patch execution and restore both failed.`);
|
|
338
|
+
if (runError) throw runError;
|
|
339
|
+
if (restoreError) throw restoreError;
|
|
340
|
+
return runResult;
|
|
341
|
+
},
|
|
183
342
|
async waitForData(predicate, options = {}) {
|
|
184
343
|
return client.waitFor(async () => {
|
|
185
344
|
try {
|
|
@@ -202,24 +361,14 @@ function createPluginHandle(client, id) {
|
|
|
202
361
|
}
|
|
203
362
|
};
|
|
204
363
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
var WaitForTimeoutError = class extends Error {
|
|
216
|
-
causeError;
|
|
217
|
-
constructor(message, causeError) {
|
|
218
|
-
super(message);
|
|
219
|
-
this.name = "WaitForTimeoutError";
|
|
220
|
-
this.causeError = causeError;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
364
|
+
function buildPluginLoadedCode(id) {
|
|
365
|
+
return [
|
|
366
|
+
"(()=>{",
|
|
367
|
+
"const __obsidianE2EPlugins=app?.plugins;",
|
|
368
|
+
`return Boolean(__obsidianE2EPlugins?.enabledPlugins?.has?.(${JSON.stringify(id)})&&__obsidianE2EPlugins?.plugins?.[${JSON.stringify(id)}]);`,
|
|
369
|
+
"})()"
|
|
370
|
+
].join("");
|
|
371
|
+
}
|
|
223
372
|
//#endregion
|
|
224
373
|
//#region src/core/transport.ts
|
|
225
374
|
const DEFAULT_TIMEOUT_MS$2 = 3e4;
|
|
@@ -266,6 +415,39 @@ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, t
|
|
|
266
415
|
return result;
|
|
267
416
|
};
|
|
268
417
|
//#endregion
|
|
418
|
+
//#region src/dev/diagnostics.ts
|
|
419
|
+
const DIAGNOSTICS_NAMESPACE = "__obsidianE2EDiagnostics";
|
|
420
|
+
function buildDiagnosticsCode(method) {
|
|
421
|
+
return [
|
|
422
|
+
"(()=>{",
|
|
423
|
+
`const __obsidianE2EMethod=${JSON.stringify(method)};`,
|
|
424
|
+
`const __obsidianE2ENamespace=${JSON.stringify(DIAGNOSTICS_NAMESPACE)};`,
|
|
425
|
+
"const __obsidianE2EMaxEntries=100;",
|
|
426
|
+
"const __obsidianE2EPush=(entries,value)=>{if(entries.length>=__obsidianE2EMaxEntries){entries.shift();}entries.push(value);};",
|
|
427
|
+
"const __obsidianE2EFormat=(value)=>{if(typeof value==='string'){return value;}try{return JSON.stringify(value);}catch{return String(value);}};",
|
|
428
|
+
"const __obsidianE2EClone=(value)=>{try{return JSON.parse(JSON.stringify(value));}catch{return __obsidianE2EFormat(value);}};",
|
|
429
|
+
"const __obsidianE2EPushRuntimeError=(source,errorLike,state)=>{const message=errorLike&&typeof errorLike==='object'&&'message' in errorLike?String(errorLike.message):String(errorLike);const stack=errorLike&&typeof errorLike==='object'&&'stack' in errorLike?String(errorLike.stack):undefined;__obsidianE2EPush(state.runtimeErrors,{at:Date.now(),message,source,stack});};",
|
|
430
|
+
"const root=globalThis;",
|
|
431
|
+
"const state=root[__obsidianE2ENamespace]??(root[__obsidianE2ENamespace]={consoleMessages:[],notices:[],runtimeErrors:[],consolePatched:false,noticePatched:false,runtimePatched:false});",
|
|
432
|
+
"if(!state.consolePatched&&root.console){for(const level of ['debug','error','info','log','warn']){const original=root.console?.[level];if(typeof original!=='function'){continue;}root.console[level]=(...args)=>{__obsidianE2EPush(state.consoleMessages,{args:args.map(__obsidianE2EClone),at:Date.now(),level,text:args.map(__obsidianE2EFormat).join(' ')});return original.apply(root.console,args);};}state.consolePatched=true;}",
|
|
433
|
+
"if(!state.runtimePatched&&typeof root.addEventListener==='function'){root.addEventListener('error',(event)=>{__obsidianE2EPushRuntimeError('error',event?.error??event?.message??'Unknown error',state);});root.addEventListener('unhandledrejection',(event)=>{__obsidianE2EPushRuntimeError('unhandledrejection',event?.reason??'Unhandled rejection',state);});state.runtimePatched=true;}",
|
|
434
|
+
"if(!state.noticePatched&&typeof root.Notice==='function'){const OriginalNotice=root.Notice;root.Notice=new Proxy(OriginalNotice,{construct(target,ctorArgs,newTarget){__obsidianE2EPush(state.notices,{at:Date.now(),message:__obsidianE2EFormat(ctorArgs[0]??''),timeout:typeof ctorArgs[1]==='number'&&Number.isFinite(ctorArgs[1])?ctorArgs[1]:undefined});return Reflect.construct(target,ctorArgs,newTarget);}});state.noticePatched=true;}",
|
|
435
|
+
"if(__obsidianE2EMethod==='reset'){state.consoleMessages.splice(0);state.notices.splice(0);state.runtimeErrors.splice(0);return true;}",
|
|
436
|
+
"if(__obsidianE2EMethod==='consoleMessages'){return state.consoleMessages;}",
|
|
437
|
+
"if(__obsidianE2EMethod==='notices'){return state.notices;}",
|
|
438
|
+
"if(__obsidianE2EMethod==='runtimeErrors'){return state.runtimeErrors;}",
|
|
439
|
+
"return {consoleMessages:state.consoleMessages,notices:state.notices,runtimeErrors:state.runtimeErrors};",
|
|
440
|
+
"})()"
|
|
441
|
+
].join("");
|
|
442
|
+
}
|
|
443
|
+
function createDevDiagnostics(value) {
|
|
444
|
+
return {
|
|
445
|
+
consoleMessages: value?.consoleMessages ?? [],
|
|
446
|
+
notices: value?.notices ?? [],
|
|
447
|
+
runtimeErrors: value?.runtimeErrors ?? []
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
269
451
|
//#region src/core/wait.ts
|
|
270
452
|
const DEFAULT_INTERVAL_MS = 100;
|
|
271
453
|
const DEFAULT_TIMEOUT_MS$1 = 5e3;
|
|
@@ -303,6 +485,7 @@ function createObsidianClient(options) {
|
|
|
303
485
|
});
|
|
304
486
|
let cachedVaultPath;
|
|
305
487
|
const client = {};
|
|
488
|
+
const metadata = createObsidianMetadataHandle(client);
|
|
306
489
|
const app = {
|
|
307
490
|
async reload(execOptions = {}) {
|
|
308
491
|
await client.exec("reload", {}, execOptions);
|
|
@@ -326,32 +509,61 @@ function createObsidianClient(options) {
|
|
|
326
509
|
}, waitOptions);
|
|
327
510
|
}
|
|
328
511
|
};
|
|
512
|
+
const dev = {
|
|
513
|
+
async activeFilePath(execOptions = {}) {
|
|
514
|
+
return this.eval("app.workspace.getActiveFile()?.path ?? null", execOptions);
|
|
515
|
+
},
|
|
516
|
+
async consoleMessages(execOptions = {}) {
|
|
517
|
+
return readDiagnosticsValue(this, "consoleMessages", execOptions);
|
|
518
|
+
},
|
|
519
|
+
async diagnostics(execOptions = {}) {
|
|
520
|
+
return createDevDiagnostics(await readDiagnosticsValue(this, "diagnostics", execOptions));
|
|
521
|
+
},
|
|
522
|
+
async dom(options, execOptions = {}) {
|
|
523
|
+
const output = await client.execText("dev:dom", {
|
|
524
|
+
all: options.all,
|
|
525
|
+
attr: options.attr,
|
|
526
|
+
css: options.css,
|
|
527
|
+
inner: options.inner,
|
|
528
|
+
selector: options.selector,
|
|
529
|
+
text: options.text,
|
|
530
|
+
total: options.total
|
|
531
|
+
}, execOptions);
|
|
532
|
+
if (options.total) return Number.parseInt(output, 10);
|
|
533
|
+
if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
|
|
534
|
+
return output;
|
|
535
|
+
},
|
|
536
|
+
async eval(code, execOptions = {}) {
|
|
537
|
+
return parseDevEvalOutput(await this.evalRaw(code, execOptions));
|
|
538
|
+
},
|
|
539
|
+
async evalJson(code, execOptions = {}) {
|
|
540
|
+
return runEvalJson(this, code, execOptions);
|
|
541
|
+
},
|
|
542
|
+
async evalRaw(code, execOptions = {}) {
|
|
543
|
+
return client.execText("eval", { code }, execOptions);
|
|
544
|
+
},
|
|
545
|
+
async editorText(execOptions = {}) {
|
|
546
|
+
return this.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null", execOptions);
|
|
547
|
+
},
|
|
548
|
+
async notices(execOptions = {}) {
|
|
549
|
+
return readDiagnosticsValue(this, "notices", execOptions);
|
|
550
|
+
},
|
|
551
|
+
async resetDiagnostics(execOptions = {}) {
|
|
552
|
+
await readDiagnosticsValue(this, "reset", execOptions);
|
|
553
|
+
},
|
|
554
|
+
async runtimeErrors(execOptions = {}) {
|
|
555
|
+
return readDiagnosticsValue(this, "runtimeErrors", execOptions);
|
|
556
|
+
},
|
|
557
|
+
async screenshot(targetPath, execOptions = {}) {
|
|
558
|
+
await client.exec("dev:screenshot", { path: targetPath }, execOptions);
|
|
559
|
+
return targetPath;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
329
562
|
Object.assign(client, {
|
|
330
563
|
app,
|
|
331
564
|
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
|
-
},
|
|
565
|
+
dev,
|
|
566
|
+
metadata,
|
|
355
567
|
command(id) {
|
|
356
568
|
return {
|
|
357
569
|
async exists(commandOptions = {}) {
|
|
@@ -419,6 +631,24 @@ function createObsidianClient(options) {
|
|
|
419
631
|
await this.vaultPath();
|
|
420
632
|
},
|
|
421
633
|
vaultName: options.vault,
|
|
634
|
+
async waitForActiveFile(path, options) {
|
|
635
|
+
return client.waitFor(async () => {
|
|
636
|
+
const activePath = await dev.activeFilePath();
|
|
637
|
+
return activePath === path ? activePath : false;
|
|
638
|
+
}, {
|
|
639
|
+
...options,
|
|
640
|
+
message: options?.message ?? `active file "${path}"`
|
|
641
|
+
});
|
|
642
|
+
},
|
|
643
|
+
async waitForConsoleMessage(predicate, options) {
|
|
644
|
+
return waitForDiagnosticEntry(client, () => client.dev.consoleMessages(), predicate, options?.message ?? "console message", options);
|
|
645
|
+
},
|
|
646
|
+
async waitForNotice(predicate, options) {
|
|
647
|
+
return waitForDiagnosticEntry(client, () => client.dev.notices(), typeof predicate === "string" ? (notice) => notice.message.includes(predicate) : predicate, options?.message ?? "notice", options);
|
|
648
|
+
},
|
|
649
|
+
async waitForRuntimeError(predicate, options) {
|
|
650
|
+
return waitForDiagnosticEntry(client, () => client.dev.runtimeErrors(), typeof predicate === "string" ? (error) => error.message.includes(predicate) : predicate, options?.message ?? "runtime error", options);
|
|
651
|
+
},
|
|
422
652
|
waitFor(fn, waitOptions) {
|
|
423
653
|
return waitForValue(fn, {
|
|
424
654
|
...waitDefaults,
|
|
@@ -435,14 +665,6 @@ function createObsidianClient(options) {
|
|
|
435
665
|
function parseCommandIds(output) {
|
|
436
666
|
return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
|
|
437
667
|
}
|
|
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
668
|
function parseTabs(output) {
|
|
447
669
|
return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
|
|
448
670
|
}
|
|
@@ -478,6 +700,19 @@ function parseWorkspace(output) {
|
|
|
478
700
|
}
|
|
479
701
|
return roots;
|
|
480
702
|
}
|
|
703
|
+
async function waitForDiagnosticEntry(client, readEntries, predicate, label, options) {
|
|
704
|
+
return client.waitFor(async () => {
|
|
705
|
+
const entries = await readEntries();
|
|
706
|
+
for (const entry of entries) if (await predicate(entry)) return entry;
|
|
707
|
+
return false;
|
|
708
|
+
}, {
|
|
709
|
+
...options,
|
|
710
|
+
message: options?.message ?? label
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
async function readDiagnosticsValue(dev, method, execOptions) {
|
|
714
|
+
return runEvalJson(dev, buildDiagnosticsCode(method), execOptions);
|
|
715
|
+
}
|
|
481
716
|
function getWorkspaceDepth(line) {
|
|
482
717
|
let depth = 0;
|
|
483
718
|
let remainder = line;
|
|
@@ -518,51 +753,188 @@ function parseWorkspaceNode(line) {
|
|
|
518
753
|
};
|
|
519
754
|
}
|
|
520
755
|
//#endregion
|
|
756
|
+
//#region src/core/path-slug.ts
|
|
757
|
+
function sanitizePathSegment(value, options = {}) {
|
|
758
|
+
const fallback = options.fallback ?? "test";
|
|
759
|
+
const maxLength = options.maxLength ?? 80;
|
|
760
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength) || fallback;
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/vault/paths.ts
|
|
764
|
+
function normalizeScope(scope) {
|
|
765
|
+
if (!scope || scope === ".") return "";
|
|
766
|
+
return scope.replace(/^\/+|\/+$/g, "");
|
|
767
|
+
}
|
|
768
|
+
function resolveVaultPath(scopeRoot, targetPath) {
|
|
769
|
+
if (!targetPath || targetPath === ".") return scopeRoot;
|
|
770
|
+
return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
|
|
771
|
+
}
|
|
772
|
+
async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
|
|
773
|
+
const vaultPath = await obsidian.vaultPath();
|
|
774
|
+
const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
|
|
775
|
+
const resolvedPath = path.resolve(vaultPath, ...relativePath);
|
|
776
|
+
const normalizedVaultPath = path.resolve(vaultPath);
|
|
777
|
+
if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
|
|
778
|
+
return resolvedPath;
|
|
779
|
+
}
|
|
780
|
+
//#endregion
|
|
781
|
+
//#region src/vault/vault.ts
|
|
782
|
+
function createVaultApi(options) {
|
|
783
|
+
const scopeRoot = normalizeScope(options.root);
|
|
784
|
+
return {
|
|
785
|
+
async delete(targetPath, deleteOptions = {}) {
|
|
786
|
+
await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
|
|
787
|
+
force: true,
|
|
788
|
+
recursive: true
|
|
789
|
+
});
|
|
790
|
+
if (deleteOptions.permanent === false) return;
|
|
791
|
+
},
|
|
792
|
+
async exists(targetPath) {
|
|
793
|
+
try {
|
|
794
|
+
await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
|
|
795
|
+
return true;
|
|
796
|
+
} catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
json(targetPath) {
|
|
801
|
+
const jsonFile = {
|
|
802
|
+
async patch(updater) {
|
|
803
|
+
const currentValue = await jsonFile.read();
|
|
804
|
+
const draft = structuredClone(currentValue);
|
|
805
|
+
const nextValue = await updater(draft) ?? draft;
|
|
806
|
+
await jsonFile.write(nextValue);
|
|
807
|
+
return nextValue;
|
|
808
|
+
},
|
|
809
|
+
async read() {
|
|
810
|
+
const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
811
|
+
return JSON.parse(rawValue);
|
|
812
|
+
},
|
|
813
|
+
async write(value) {
|
|
814
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
815
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
816
|
+
await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
return jsonFile;
|
|
820
|
+
},
|
|
821
|
+
async mkdir(targetPath) {
|
|
822
|
+
await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
|
|
823
|
+
},
|
|
824
|
+
async read(targetPath) {
|
|
825
|
+
return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
|
|
826
|
+
},
|
|
827
|
+
async waitForContent(targetPath, predicate, waitOptions = {}) {
|
|
828
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
829
|
+
return options.obsidian.waitFor(async () => {
|
|
830
|
+
try {
|
|
831
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
832
|
+
return await predicate(content) ? content : false;
|
|
833
|
+
} catch {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
}, {
|
|
837
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
|
|
838
|
+
...waitOptions
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
async waitForExists(targetPath, waitOptions) {
|
|
842
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
843
|
+
await options.obsidian.waitFor(async () => {
|
|
844
|
+
try {
|
|
845
|
+
await access(resolvedPath);
|
|
846
|
+
return true;
|
|
847
|
+
} catch {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}, {
|
|
851
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
|
|
852
|
+
...waitOptions
|
|
853
|
+
});
|
|
854
|
+
},
|
|
855
|
+
async waitForMissing(targetPath, waitOptions) {
|
|
856
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
857
|
+
await options.obsidian.waitFor(async () => {
|
|
858
|
+
try {
|
|
859
|
+
await access(resolvedPath);
|
|
860
|
+
return false;
|
|
861
|
+
} catch {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
}, {
|
|
865
|
+
message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
|
|
866
|
+
...waitOptions
|
|
867
|
+
});
|
|
868
|
+
},
|
|
869
|
+
async write(targetPath, content, writeOptions = {}) {
|
|
870
|
+
const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
|
|
871
|
+
await mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
872
|
+
await writeFile(resolvedPath, content, "utf8");
|
|
873
|
+
if (!writeOptions.waitForContent) return;
|
|
874
|
+
const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
|
|
875
|
+
await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
521
880
|
//#region src/artifacts/failure-artifacts.ts
|
|
522
881
|
const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
|
|
882
|
+
const DEFAULT_FAILURE_ARTIFACT_CAPTURE = {
|
|
883
|
+
activeFile: true,
|
|
884
|
+
activeNote: true,
|
|
885
|
+
consoleMessages: true,
|
|
886
|
+
dom: true,
|
|
887
|
+
editorText: true,
|
|
888
|
+
notices: true,
|
|
889
|
+
parsedFrontmatter: true,
|
|
890
|
+
runtimeErrors: true,
|
|
891
|
+
screenshot: true,
|
|
892
|
+
tabs: true,
|
|
893
|
+
workspace: true
|
|
894
|
+
};
|
|
523
895
|
function getFailureArtifactConfig(options) {
|
|
524
896
|
if (!options.captureOnFailure) return {
|
|
525
897
|
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
|
-
},
|
|
898
|
+
capture: { ...DEFAULT_FAILURE_ARTIFACT_CAPTURE },
|
|
534
899
|
enabled: false
|
|
535
900
|
};
|
|
536
901
|
const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
|
|
537
902
|
return {
|
|
538
903
|
artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
|
|
539
904
|
capture: {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
editorText: overrides.editorText ?? true,
|
|
543
|
-
screenshot: overrides.screenshot ?? true,
|
|
544
|
-
tabs: overrides.tabs ?? true,
|
|
545
|
-
workspace: overrides.workspace ?? true
|
|
905
|
+
...DEFAULT_FAILURE_ARTIFACT_CAPTURE,
|
|
906
|
+
...overrides
|
|
546
907
|
},
|
|
547
908
|
enabled: true
|
|
548
909
|
};
|
|
549
910
|
}
|
|
550
911
|
function getFailureArtifactDirectory(artifactsDir, task) {
|
|
551
912
|
const suffix = task.id.split("_").at(-1) ?? "test";
|
|
552
|
-
return path.join(artifactsDir, `${
|
|
913
|
+
return path.join(artifactsDir, `${sanitizePathSegment(task.name, { maxLength: 60 })}-${suffix}`);
|
|
553
914
|
}
|
|
554
915
|
async function captureFailureArtifacts(task, obsidian, options) {
|
|
555
916
|
const config = getFailureArtifactConfig(options);
|
|
556
917
|
if (!config.enabled) return;
|
|
557
918
|
const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
|
|
558
919
|
await mkdir(artifactDirectory, { recursive: true });
|
|
920
|
+
const activeFile = readArtifactInput(() => readActiveFilePath(obsidian));
|
|
921
|
+
const activeNote = readArtifactInput(async () => {
|
|
922
|
+
const activeFilePath = await unwrapArtifactInput(activeFile);
|
|
923
|
+
return activeFilePath ? readActiveNoteSnapshot(obsidian, activeFilePath) : null;
|
|
924
|
+
});
|
|
925
|
+
const diagnostics = await obsidian.dev.diagnostics().catch(() => null);
|
|
559
926
|
await Promise.all([
|
|
560
|
-
captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await
|
|
927
|
+
captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await unwrapArtifactInput(activeFile) })),
|
|
928
|
+
captureTextArtifact(artifactDirectory, "active-note.md", config.capture.activeNote, async () => (await unwrapArtifactInput(activeNote))?.raw ?? ""),
|
|
929
|
+
captureJsonArtifact(artifactDirectory, "active-note-frontmatter.json", config.capture.parsedFrontmatter, async () => ({ frontmatter: (await unwrapArtifactInput(activeNote))?.frontmatter ?? null })),
|
|
561
930
|
captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
|
|
562
931
|
inner: true,
|
|
563
932
|
selector: ".workspace"
|
|
564
933
|
}))),
|
|
565
|
-
captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.
|
|
934
|
+
captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.editorText() })),
|
|
935
|
+
captureJsonArtifact(artifactDirectory, "console-messages.json", config.capture.consoleMessages, async () => diagnostics?.consoleMessages ?? []),
|
|
936
|
+
captureJsonArtifact(artifactDirectory, "runtime-errors.json", config.capture.runtimeErrors, async () => diagnostics?.runtimeErrors ?? []),
|
|
937
|
+
captureJsonArtifact(artifactDirectory, "notices.json", config.capture.notices, async () => diagnostics?.notices ?? []),
|
|
566
938
|
captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
|
|
567
939
|
captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
|
|
568
940
|
captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
|
|
@@ -570,14 +942,6 @@ async function captureFailureArtifacts(task, obsidian, options) {
|
|
|
570
942
|
]);
|
|
571
943
|
return artifactDirectory;
|
|
572
944
|
}
|
|
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
945
|
async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
|
|
582
946
|
if (!enabled) return;
|
|
583
947
|
try {
|
|
@@ -607,8 +971,59 @@ async function captureTextArtifact(artifactDirectory, filename, enabled, readVal
|
|
|
607
971
|
function formatArtifactError(error) {
|
|
608
972
|
return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
|
|
609
973
|
}
|
|
610
|
-
function
|
|
611
|
-
return
|
|
974
|
+
async function readActiveFilePath(obsidian) {
|
|
975
|
+
return obsidian.dev.activeFilePath();
|
|
976
|
+
}
|
|
977
|
+
async function readActiveNoteSnapshot(obsidian, activeFile) {
|
|
978
|
+
return parseNoteDocument(await createVaultApi({ obsidian }).read(activeFile));
|
|
979
|
+
}
|
|
980
|
+
function readArtifactInput(readValue) {
|
|
981
|
+
return { promise: readValue().then((value) => ({
|
|
982
|
+
ok: true,
|
|
983
|
+
value
|
|
984
|
+
}), (error) => ({
|
|
985
|
+
error,
|
|
986
|
+
ok: false
|
|
987
|
+
})) };
|
|
988
|
+
}
|
|
989
|
+
async function unwrapArtifactInput(result) {
|
|
990
|
+
const value = await result.promise;
|
|
991
|
+
if (!value.ok) throw value.error;
|
|
992
|
+
return value.value;
|
|
993
|
+
}
|
|
994
|
+
//#endregion
|
|
995
|
+
//#region src/vault/sandbox.ts
|
|
996
|
+
async function createSandboxApi(options) {
|
|
997
|
+
const root = posix.join(options.sandboxRoot, `${sanitizePathSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
|
|
998
|
+
const sandboxPath = (...segments) => posix.join(root, ...segments);
|
|
999
|
+
const vault = createVaultApi({
|
|
1000
|
+
obsidian: options.obsidian,
|
|
1001
|
+
root
|
|
1002
|
+
});
|
|
1003
|
+
await vault.mkdir(".");
|
|
1004
|
+
return {
|
|
1005
|
+
...vault,
|
|
1006
|
+
async cleanup() {
|
|
1007
|
+
await vault.delete(".", { permanent: true });
|
|
1008
|
+
},
|
|
1009
|
+
path(...segments) {
|
|
1010
|
+
return sandboxPath(...segments);
|
|
1011
|
+
},
|
|
1012
|
+
async readNote(targetPath) {
|
|
1013
|
+
return parseNoteDocument(await vault.read(targetPath));
|
|
1014
|
+
},
|
|
1015
|
+
root,
|
|
1016
|
+
async writeNote(writeOptions) {
|
|
1017
|
+
const { path, waitForMetadata = true, waitOptions, ...noteInput } = writeOptions;
|
|
1018
|
+
const document = createNoteDocument(noteInput);
|
|
1019
|
+
await vault.write(path, document.raw);
|
|
1020
|
+
if (waitForMetadata) {
|
|
1021
|
+
const predicate = typeof waitForMetadata === "function" ? waitForMetadata : void 0;
|
|
1022
|
+
await options.obsidian.metadata.waitForMetadata(sandboxPath(path), predicate, waitOptions);
|
|
1023
|
+
}
|
|
1024
|
+
return document;
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
612
1027
|
}
|
|
613
1028
|
//#endregion
|
|
614
1029
|
//#region src/fixtures/vault-lock.ts
|
|
@@ -772,147 +1187,167 @@ async function sleep(durationMs) {
|
|
|
772
1187
|
async function writeMetadata(metadataPath, metadata) {
|
|
773
1188
|
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
774
1189
|
}
|
|
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);
|
|
1190
|
+
function createBaseFixtures(options, fixtureOptions = {}) {
|
|
1191
|
+
const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
|
|
797
1192
|
return {
|
|
798
|
-
async
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1193
|
+
_vaultLock: [async ({}, use) => {
|
|
1194
|
+
if (!options.sharedVaultLock) {
|
|
1195
|
+
await use(null);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const lockClient = createObsidianClient(options);
|
|
1199
|
+
await lockClient.verify();
|
|
1200
|
+
const vaultLock = await acquireVaultRunLock({
|
|
1201
|
+
...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
|
|
1202
|
+
vaultName: options.vault,
|
|
1203
|
+
vaultPath: await lockClient.vaultPath()
|
|
802
1204
|
});
|
|
803
|
-
|
|
804
|
-
},
|
|
805
|
-
async exists(targetPath) {
|
|
1205
|
+
await vaultLock.publishMarker(lockClient);
|
|
806
1206
|
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 () => {
|
|
1207
|
+
await use(vaultLock);
|
|
1208
|
+
} finally {
|
|
843
1209
|
try {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1210
|
+
await clearVaultRunLockMarker(lockClient);
|
|
1211
|
+
} catch {}
|
|
1212
|
+
await vaultLock.release();
|
|
1213
|
+
}
|
|
1214
|
+
}, { scope: "worker" }],
|
|
1215
|
+
_testContext: async ({ _vaultLock, onTestFailed, task }, use) => {
|
|
1216
|
+
let failedTask = false;
|
|
1217
|
+
onTestFailed(() => {
|
|
1218
|
+
failedTask = true;
|
|
852
1219
|
});
|
|
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
|
|
1220
|
+
const context = await createInternalTestContext({
|
|
1221
|
+
...options,
|
|
1222
|
+
createVault,
|
|
1223
|
+
testName: task.name,
|
|
1224
|
+
vaultLock: _vaultLock
|
|
866
1225
|
});
|
|
1226
|
+
try {
|
|
1227
|
+
await use(context);
|
|
1228
|
+
} finally {
|
|
1229
|
+
await context.cleanup({ failedTask: failedTask ? task : void 0 });
|
|
1230
|
+
}
|
|
867
1231
|
},
|
|
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
|
-
});
|
|
1232
|
+
obsidian: async ({ _testContext }, use) => {
|
|
1233
|
+
await use(_testContext.obsidian);
|
|
881
1234
|
},
|
|
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);
|
|
1235
|
+
sandbox: async ({ _testContext }, use) => {
|
|
1236
|
+
await use(_testContext.sandbox);
|
|
1237
|
+
},
|
|
1238
|
+
vault: async ({ _testContext }, use) => {
|
|
1239
|
+
await use(_testContext.vault);
|
|
889
1240
|
}
|
|
890
1241
|
};
|
|
891
1242
|
}
|
|
892
1243
|
//#endregion
|
|
893
|
-
//#region src/
|
|
894
|
-
async function
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1244
|
+
//#region src/fixtures/test-context.ts
|
|
1245
|
+
async function createTestContext(options) {
|
|
1246
|
+
return createInternalTestContext(options);
|
|
1247
|
+
}
|
|
1248
|
+
async function withVaultSandbox(options, run) {
|
|
1249
|
+
const context = await createInternalTestContext(options);
|
|
1250
|
+
try {
|
|
1251
|
+
return await run(context);
|
|
1252
|
+
} finally {
|
|
1253
|
+
await context.cleanup();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function createInternalTestContext(options) {
|
|
1257
|
+
const obsidian = createObsidianClient(options);
|
|
1258
|
+
const trackedPlugins = /* @__PURE__ */ new Map();
|
|
1259
|
+
let sandbox = null;
|
|
1260
|
+
const vaultLock = options.vaultLock ?? await maybeAcquireVaultLock(options, obsidian);
|
|
1261
|
+
const ownsVaultLock = options.vaultLock === void 0 && vaultLock !== null;
|
|
1262
|
+
const vaultFactory = options.createVault ?? ((client) => createVaultApi({ obsidian: client }));
|
|
1263
|
+
let disposed = false;
|
|
1264
|
+
try {
|
|
1265
|
+
await obsidian.verify();
|
|
1266
|
+
if (vaultLock) await vaultLock.publishMarker(obsidian);
|
|
1267
|
+
await obsidian.dev.resetDiagnostics().catch(() => {});
|
|
1268
|
+
const vault = await vaultFactory(obsidian);
|
|
1269
|
+
sandbox = await createSandboxApi({
|
|
1270
|
+
obsidian,
|
|
1271
|
+
sandboxRoot: options.sandboxRoot ?? "__obsidian_e2e__",
|
|
1272
|
+
testName: options.testName ?? "test"
|
|
1273
|
+
});
|
|
1274
|
+
const captureArtifacts = async (task) => captureFailureArtifacts(task, obsidian, {
|
|
1275
|
+
...options,
|
|
1276
|
+
plugin: trackedPlugins.size === 1 ? [...trackedPlugins.values()][0].plugin : void 0
|
|
1277
|
+
});
|
|
1278
|
+
const cleanup = async (cleanupOptions = {}) => {
|
|
1279
|
+
if (disposed) return;
|
|
1280
|
+
disposed = true;
|
|
1281
|
+
const cleanupErrors = [];
|
|
1282
|
+
try {
|
|
1283
|
+
if (cleanupOptions.failedTask && options.captureOnFailure) await captureArtifacts(cleanupOptions.failedTask);
|
|
1284
|
+
} finally {
|
|
1285
|
+
try {
|
|
1286
|
+
await getClientInternals(obsidian).restoreAll();
|
|
1287
|
+
} finally {
|
|
1288
|
+
for (const session of [...trackedPlugins.values()].reverse()) if (!session.wasEnabled) await recordCleanupError(cleanupErrors, async () => session.plugin.disable({ filter: session.filter }));
|
|
1289
|
+
try {
|
|
1290
|
+
await clearVaultRunLockMarker(obsidian);
|
|
1291
|
+
} catch {}
|
|
1292
|
+
if (ownsVaultLock) await recordCleanupError(cleanupErrors, async () => vaultLock.release());
|
|
1293
|
+
await recordCleanupError(cleanupErrors, async () => sandbox.cleanup());
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
|
1297
|
+
if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "One or more test cleanup steps failed.");
|
|
1298
|
+
};
|
|
1299
|
+
return {
|
|
1300
|
+
obsidian,
|
|
1301
|
+
sandbox,
|
|
1302
|
+
vault,
|
|
1303
|
+
captureFailureArtifacts: captureArtifacts,
|
|
1304
|
+
cleanup,
|
|
1305
|
+
async plugin(id, sessionOptions = {}) {
|
|
1306
|
+
const existing = trackedPlugins.get(id);
|
|
1307
|
+
if (existing) return existing.plugin;
|
|
1308
|
+
const plugin = obsidian.plugin(id);
|
|
1309
|
+
const wasEnabled = await plugin.isEnabled();
|
|
1310
|
+
if (!wasEnabled) await plugin.enable({ filter: sessionOptions.filter });
|
|
1311
|
+
if (sessionOptions.seedData !== void 0) await plugin.data().write(sessionOptions.seedData);
|
|
1312
|
+
trackedPlugins.set(id, {
|
|
1313
|
+
filter: sessionOptions.filter,
|
|
1314
|
+
plugin,
|
|
1315
|
+
wasEnabled
|
|
1316
|
+
});
|
|
1317
|
+
return plugin;
|
|
1318
|
+
},
|
|
1319
|
+
async resetDiagnostics() {
|
|
1320
|
+
await obsidian.dev.resetDiagnostics().catch(() => {});
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
try {
|
|
1325
|
+
if (sandbox) await sandbox.cleanup();
|
|
1326
|
+
} finally {
|
|
1327
|
+
try {
|
|
1328
|
+
await clearVaultRunLockMarker(obsidian);
|
|
1329
|
+
} catch {}
|
|
1330
|
+
if (ownsVaultLock) await vaultLock.release();
|
|
1331
|
+
}
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
911
1334
|
}
|
|
912
|
-
function
|
|
913
|
-
|
|
1335
|
+
async function recordCleanupError(cleanupErrors, run) {
|
|
1336
|
+
try {
|
|
1337
|
+
await run();
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
cleanupErrors.push(error);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
async function maybeAcquireVaultLock(options, obsidian) {
|
|
1343
|
+
if (!options.sharedVaultLock) return null;
|
|
1344
|
+
return acquireVaultRunLock({
|
|
1345
|
+
...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
|
|
1346
|
+
vaultName: options.vault,
|
|
1347
|
+
vaultPath: await obsidian.vaultPath()
|
|
1348
|
+
});
|
|
914
1349
|
}
|
|
915
1350
|
//#endregion
|
|
916
|
-
export { clearVaultRunLockMarker as a,
|
|
1351
|
+
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
1352
|
|
|
918
|
-
//# sourceMappingURL=
|
|
1353
|
+
//# sourceMappingURL=test-context-Bl-e-83H.mjs.map
|