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 CHANGED
@@ -207,7 +207,13 @@ import { test } from "./setup";
207
207
  test("reloads a plugin after patching its data file", async ({ obsidian, vault, sandbox }) => {
208
208
  const plugin = obsidian.plugin("my-plugin");
209
209
 
210
- await sandbox.write("tpl.md", "template body");
210
+ await sandbox.writeNote({
211
+ path: "tpl.md",
212
+ frontmatter: {
213
+ tags: ["template"],
214
+ },
215
+ body: "template body",
216
+ });
211
217
  await vault.write("notes/source.md", "existing");
212
218
 
213
219
  await plugin.data<{ enabled: boolean }>().patch((draft) => {
@@ -231,10 +237,69 @@ Fixture summary:
231
237
  - `sandbox`
232
238
  - a per-test disposable directory under `sandboxRoot`; automatically cleaned
233
239
  up after each test
240
+ - exposes note helpers such as `writeNote()`, `readNote()`, and `path()`
234
241
 
235
242
  Plugin data mutations are snapshotted on first write and restored automatically
236
243
  after each test. Sandbox files are also cleaned up automatically.
237
244
 
245
+ ## Note Helpers And Test Context
246
+
247
+ Use `sandbox.writeNote()` when the test cares about note structure rather than
248
+ raw YAML formatting:
249
+
250
+ ```ts
251
+ await sandbox.writeNote({
252
+ path: "Inbox/Today.md",
253
+ frontmatter: {
254
+ mood: "focused",
255
+ tags: ["daily"],
256
+ },
257
+ body: "# Today\n",
258
+ });
259
+
260
+ await expect(sandbox.readNote("Inbox/Today.md")).resolves.toMatchObject({
261
+ body: "# Today\n",
262
+ frontmatter: {
263
+ mood: "focused",
264
+ tags: ["daily"],
265
+ },
266
+ });
267
+
268
+ await obsidian.metadata.waitForFrontmatter(sandbox.path("Inbox/Today.md"), (frontmatter) =>
269
+ frontmatter.tags.includes("daily"),
270
+ );
271
+ ```
272
+
273
+ `readNote()` is file-derived. Metadata-cache reads stay under
274
+ `obsidian.metadata.*`, so tests can distinguish raw file content from
275
+ “Obsidian has indexed this note”.
276
+
277
+ Outside Vitest fixtures, use the public lifecycle wrapper:
278
+
279
+ ```ts
280
+ import { withVaultSandbox } from "obsidian-e2e";
281
+
282
+ await withVaultSandbox(
283
+ {
284
+ testName: "quickadd smoke",
285
+ vault: "dev",
286
+ },
287
+ async (context) => {
288
+ const plugin = await context.plugin("quickadd", {
289
+ filter: "community",
290
+ seedData: { enabled: true },
291
+ });
292
+
293
+ await context.sandbox.writeNote({
294
+ path: "fixtures/template.md",
295
+ body: "Hello from template",
296
+ });
297
+
298
+ await plugin.reload();
299
+ },
300
+ );
301
+ ```
302
+
238
303
  ## Plugin Test Helper
239
304
 
240
305
  If you are testing one plugin repeatedly, `createPluginTest()` gives you a
@@ -250,7 +315,14 @@ export const test = createPluginTest({
250
315
  pluginFilter: "community",
251
316
  seedPluginData: { enabled: true },
252
317
  seedVault: {
253
- "fixtures/template.md": "template body",
318
+ "fixtures/template.md": {
319
+ note: {
320
+ body: "template body",
321
+ frontmatter: {
322
+ tags: ["template"],
323
+ },
324
+ },
325
+ },
254
326
  "fixtures/state.json": { json: { ready: true } },
255
327
  },
256
328
  });
@@ -262,6 +334,7 @@ export const test = createPluginTest({
262
334
  - enables the target plugin for the test when needed and restores the prior
263
335
  enabled/disabled state afterward
264
336
  - seeds vault files before each test and restores the original files afterward
337
+ - `seedVault` accepts raw strings, `{ json }`, and `{ note }` descriptors
265
338
  - seeds `data.json` through the normal plugin snapshot/restore path
266
339
  - supports the same opt-in failure artifact capture as `createObsidianTest()`
267
340
 
@@ -321,8 +394,13 @@ task-id suffix, for example:
321
394
  `createObsidianTest()` captures:
322
395
 
323
396
  - `active-file.json`
397
+ - `active-note.md`
398
+ - `active-note-frontmatter.json`
399
+ - `console-messages.json`
324
400
  - `dom.txt`
325
401
  - `editor.json`
402
+ - `notices.json`
403
+ - `runtime-errors.json`
326
404
  - `tabs.json`
327
405
  - `workspace.json`
328
406
  - `screenshot.png` when screenshot capture succeeds
@@ -395,7 +473,9 @@ Import `obsidian-e2e/matchers` once in your test setup to register:
395
473
  - `toHaveEditorTextContaining(needle)`
396
474
  - `toHaveFile(path)`
397
475
  - `toHaveFileContaining(path, needle)`
476
+ - `toHaveFrontmatter(path, expected)`
398
477
  - `toHaveJsonFile(path)`
478
+ - `toHaveNote(path, { frontmatter?, body?, bodyIncludes? })`
399
479
  - `toHaveOpenTab(title, viewType?)`
400
480
  - `toHavePluginData(expected)`
401
481
  - `toHaveWorkspaceNode(label)`
@@ -407,9 +487,23 @@ import { expect } from "vite-plus/test";
407
487
  import { test } from "./setup";
408
488
 
409
489
  test("writes valid JSON into the sandbox", async ({ sandbox }) => {
410
- await sandbox.json("config.json").write({ enabled: true });
490
+ await sandbox.writeNote({
491
+ path: "Today.md",
492
+ frontmatter: {
493
+ mood: "focused",
494
+ },
495
+ body: "# Today\n",
496
+ });
411
497
 
412
- await expect(sandbox).toHaveJsonFile("config.json");
498
+ await expect(sandbox).toHaveNote("Today.md", {
499
+ bodyIncludes: "Today",
500
+ frontmatter: {
501
+ mood: "focused",
502
+ },
503
+ });
504
+ await expect(sandbox).toHaveFrontmatter("Today.md", {
505
+ mood: "focused",
506
+ });
413
507
  });
414
508
 
415
509
  test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
@@ -427,7 +521,7 @@ test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
427
521
  If you need to work below the fixture layer:
428
522
 
429
523
  ```ts
430
- import { createObsidianClient, createVaultApi } from "obsidian-e2e";
524
+ import { createObsidianClient, createVaultApi, parseNoteDocument } from "obsidian-e2e";
431
525
 
432
526
  const obsidian = createObsidianClient({
433
527
  vault: "dev",
@@ -440,12 +534,14 @@ const vault = createVaultApi({ obsidian });
440
534
 
441
535
  await obsidian.verify();
442
536
  await vault.write("Inbox/Today.md", "# Today\n", { waitForContent: true });
537
+ await expect(await obsidian.metadata.frontmatter("Inbox/Today.md")).toBeNull();
443
538
  await obsidian.plugin("my-plugin").reload({
444
539
  waitUntilReady: true,
445
540
  readyOptions: {
446
541
  commandId: "my-plugin:refresh",
447
542
  },
448
543
  });
544
+ parseNoteDocument(await vault.read("Inbox/Today.md"));
449
545
  ```
450
546
 
451
547
  ## App And Commands
@@ -462,12 +558,25 @@ to the real `obsidian` CLI:
462
558
  - `obsidian.command(id).run()`
463
559
  - `obsidian.dev.dom({ ... })`
464
560
  - `obsidian.dev.eval(code)`
561
+ - `obsidian.dev.evalJson(code)`
562
+ - `obsidian.dev.evalRaw(code)`
563
+ - `obsidian.dev.diagnostics()`
564
+ - `obsidian.dev.resetDiagnostics()`
565
+ - `obsidian.metadata.fileCache(path)`
566
+ - `obsidian.metadata.frontmatter(path)`
567
+ - `obsidian.metadata.waitForFileCache(path, predicate?)`
568
+ - `obsidian.metadata.waitForFrontmatter(path, predicate?)`
569
+ - `obsidian.metadata.waitForMetadata(path, predicate?)`
465
570
  - `obsidian.dev.screenshot(path)`
466
571
  - `obsidian.tabs()`
467
572
  - `obsidian.workspace()`
468
573
  - `obsidian.open({ file? | path?, newTab? })`
469
574
  - `obsidian.openTab({ file?, group?, view? })`
470
575
  - `obsidian.sleep(ms)`
576
+ - `obsidian.waitForActiveFile(path)`
577
+ - `obsidian.waitForConsoleMessage(predicate)`
578
+ - `obsidian.waitForNotice(predicate)`
579
+ - `obsidian.waitForRuntimeError(predicate)`
471
580
 
472
581
  Example:
473
582
 
@@ -495,14 +604,14 @@ test("reloads the app and runs a plugin command when it becomes available", asyn
495
604
  `obsidian.app.restart()` waits for the app to come back by default. Pass
496
605
  `{ waitUntilReady: false }` if you need to manage readiness explicitly.
497
606
 
498
- ## Vault And Plugin Wait Helpers
607
+ ## Vault, Metadata, And Plugin Wait Helpers
499
608
 
500
609
  The higher-level vault and plugin handles now expose the most common polling
501
610
  patterns directly, so tests do not need to hand-roll `waitFor()` loops around
502
611
  content reads, command discovery, or plugin data migration:
503
612
 
504
613
  ```ts
505
- test("waits for generated content and plugin state", async ({ obsidian, vault }) => {
614
+ test("waits for generated content and plugin state", async ({ obsidian, sandbox, vault }) => {
506
615
  const plugin = obsidian.plugin("quickadd");
507
616
 
508
617
  await vault.write("queue.md", "pending", {
@@ -510,12 +619,19 @@ test("waits for generated content and plugin state", async ({ obsidian, vault })
510
619
  });
511
620
 
512
621
  await vault.waitForContent("queue.md", (content) => content.includes("pending"));
513
-
514
- await plugin.reload({
515
- waitUntilReady: true,
516
- readyOptions: {
517
- commandId: "quickadd:run-choice",
622
+ await sandbox.writeNote({
623
+ path: "Inbox/Today.md",
624
+ frontmatter: {
625
+ tags: ["daily"],
518
626
  },
627
+ body: "# Today\n",
628
+ });
629
+ await obsidian.metadata.waitForFrontmatter(sandbox.path("Inbox/Today.md"), (frontmatter) =>
630
+ frontmatter.tags.includes("daily"),
631
+ );
632
+
633
+ await plugin.updateDataAndReload<{ migrations: Record<string, boolean> }>((draft) => {
634
+ draft.migrations.quickadd_v2 = true;
519
635
  });
520
636
 
521
637
  await plugin.waitForData<{ migrations: Record<string, boolean> }>(
@@ -562,11 +678,47 @@ test("inspects live UI state", async ({ obsidian }) => {
562
678
  });
563
679
  ```
564
680
 
565
- `obsidian.dev.eval()` is the low-level escape hatch, while `dev.dom()` and
566
- `dev.screenshot()` give you safer wrappers around the built-in developer CLI
681
+ `obsidian.dev.eval()` remains the low-level escape hatch and preserves the raw
682
+ CLI parsing behavior. Use `obsidian.dev.evalJson()` when you want JSON-safe
683
+ typed results and remote error details, and `obsidian.dev.evalRaw()` when you
684
+ intentionally need the unstructured CLI output. `dev.dom()` and
685
+ `dev.screenshot()` remain the safer wrappers around the built-in developer CLI
567
686
  commands. Screenshot behavior depends on the active desktop environment, so
568
687
  start by validating it locally before relying on it in automation.
569
688
 
689
+ ## Layer Boundaries
690
+
691
+ - `vault` stays filesystem-only. If the behavior depends on Obsidian parsing or
692
+ workspace state, it does not belong there.
693
+ - `sandbox.readNote()` parses file content only. It does not imply that
694
+ Obsidian has indexed the note.
695
+ - `obsidian.metadata.*` reads metadata-cache state, which is the right layer
696
+ for frontmatter synchronization and race-sensitive tests.
697
+ - `obsidian.dev.eval()` is the escape hatch. Prefer the higher-level metadata,
698
+ sandbox, wait, plugin, and matcher helpers first, and use
699
+ `obsidian.dev.evalJson()` when you need structured JSON-safe results.
700
+
701
+ ## Migration Notes
702
+
703
+ - Keep using `obsidian.dev.eval()` for the raw escape hatch semantics.
704
+ - Use `obsidian.dev.evalJson()` when you want JSON-safe typed results and
705
+ `DevEvalError` stack details.
706
+ - Use `obsidian.metadata.*` for metadata-cache synchronization, including notes
707
+ created under `sandbox.path(...)`.
708
+ - Prefer `sandbox.writeNote()` over hand-built YAML strings when the test is
709
+ describing note content rather than string formatting.
710
+ - Prefer `plugin.updateDataAndReload()` or `plugin.withPatchedData()` over open-
711
+ coded patch/reload/restore sequences.
712
+
713
+ ## Regression Matrix
714
+
715
+ - Metadata waits cover delayed file-cache population and frontmatter
716
+ synchronization after note writes.
717
+ - Failure artifacts capture active note content, parsed frontmatter, recent
718
+ console/notices/runtime errors, and workspace snapshots.
719
+ - Lifecycle cleanup restores tracked plugin data before disabling plugins and
720
+ removes sandbox content after teardown.
721
+
570
722
  ## End-To-End Workflow
571
723
 
572
724
  Putting it together, a realistic plugin test usually looks like this:
@@ -0,0 +1,69 @@
1
+ import { parse, stringify } from "yaml";
2
+ //#region src/note/document.ts
3
+ function parseNoteDocument(raw) {
4
+ const normalizedRaw = normalizeLineEndings(raw);
5
+ const parsed = parseFrontmatterBlock(normalizedRaw);
6
+ if (!parsed) return {
7
+ body: normalizedRaw,
8
+ frontmatter: null,
9
+ raw: normalizedRaw
10
+ };
11
+ const parsedFrontmatter = parse(parsed.frontmatterRaw);
12
+ if (parsedFrontmatter !== null && (typeof parsedFrontmatter !== "object" || Array.isArray(parsedFrontmatter))) throw new Error("Expected note frontmatter to be a YAML mapping object.");
13
+ return {
14
+ body: normalizeLineEndings(parsed.body),
15
+ frontmatter: parsedFrontmatter ?? {},
16
+ raw: normalizedRaw
17
+ };
18
+ }
19
+ function stringifyNoteDocument(input) {
20
+ const normalizedBody = normalizeLineEndings(input.body ?? "");
21
+ if (!input.frontmatter) return normalizedBody;
22
+ const frontmatterYaml = stringify(input.frontmatter).trimEnd();
23
+ return `${frontmatterYaml ? `---\n${frontmatterYaml}\n---\n` : "---\n---\n"}${normalizedBody}`;
24
+ }
25
+ function createNoteDocument(input) {
26
+ const body = normalizeLineEndings(input.body ?? "");
27
+ if (input.frontmatter === void 0 || input.frontmatter === null) return {
28
+ body,
29
+ frontmatter: null,
30
+ raw: stringifyNoteDocument({ body })
31
+ };
32
+ const frontmatter = cloneFrontmatter(input.frontmatter);
33
+ return {
34
+ body,
35
+ frontmatter,
36
+ raw: stringifyNoteDocument({
37
+ body,
38
+ frontmatter
39
+ })
40
+ };
41
+ }
42
+ function normalizeLineEndings(value) {
43
+ return value.replace(/\r\n?/gu, "\n");
44
+ }
45
+ function cloneFrontmatter(frontmatter) {
46
+ return JSON.parse(JSON.stringify(frontmatter));
47
+ }
48
+ function parseFrontmatterBlock(raw) {
49
+ if (!raw.startsWith("---")) return null;
50
+ const openingMatch = raw.match(/^---(?:\n|$)/u);
51
+ if (!openingMatch) return null;
52
+ const openLength = openingMatch[0].length;
53
+ const closingMatcher = /(?:^|\n)(?:---|\.\.\.)(?:\n|$)/gu;
54
+ closingMatcher.lastIndex = openLength;
55
+ const closingMatch = closingMatcher.exec(raw);
56
+ if (!closingMatch) return null;
57
+ const closingStart = closingMatch.index + (closingMatch[0].startsWith("\n") ? 1 : 0);
58
+ const closingLength = closingMatch[0].startsWith("\n") ? closingMatch[0].length - 1 : closingMatch[0].length;
59
+ const frontmatterRaw = raw.slice(openLength, closingStart);
60
+ const bodyStart = closingStart + closingLength;
61
+ return {
62
+ body: raw.slice(bodyStart),
63
+ frontmatterRaw: frontmatterRaw.trimEnd()
64
+ };
65
+ }
66
+ //#endregion
67
+ export { parseNoteDocument as n, stringifyNoteDocument as r, createNoteDocument as t };
68
+
69
+ //# sourceMappingURL=document-DunL2Moz.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-DunL2Moz.mjs","names":["parseYaml","stringifyYaml"],"sources":["../src/note/document.ts"],"sourcesContent":["import { parse as parseYaml, stringify as stringifyYaml } from \"yaml\";\n\nimport type { NoteDocument, NoteFrontmatter, NoteInput } from \"../core/types\";\n\nexport function parseNoteDocument(raw: string): NoteDocument {\n const normalizedRaw = normalizeLineEndings(raw);\n const parsed = parseFrontmatterBlock(normalizedRaw);\n\n if (!parsed) {\n return {\n body: normalizedRaw,\n frontmatter: null,\n raw: normalizedRaw,\n };\n }\n\n const parsedFrontmatter = parseYaml(parsed.frontmatterRaw);\n\n if (\n parsedFrontmatter !== null &&\n (typeof parsedFrontmatter !== \"object\" || Array.isArray(parsedFrontmatter))\n ) {\n throw new Error(\"Expected note frontmatter to be a YAML mapping object.\");\n }\n\n return {\n body: normalizeLineEndings(parsed.body),\n frontmatter: (parsedFrontmatter ?? {}) as NoteFrontmatter,\n raw: normalizedRaw,\n };\n}\n\nexport function stringifyNoteDocument(input: NoteInput): string {\n const normalizedBody = normalizeLineEndings(input.body ?? \"\");\n\n if (!input.frontmatter) {\n return normalizedBody;\n }\n\n const frontmatterYaml = stringifyYaml(input.frontmatter).trimEnd();\n const frontmatterSection = frontmatterYaml ? `---\\n${frontmatterYaml}\\n---\\n` : \"---\\n---\\n\";\n\n return `${frontmatterSection}${normalizedBody}`;\n}\n\nexport function createNoteDocument(input: NoteInput): NoteDocument {\n const body = normalizeLineEndings(input.body ?? \"\");\n\n if (input.frontmatter === undefined || input.frontmatter === null) {\n return {\n body,\n frontmatter: null,\n raw: stringifyNoteDocument({ body }),\n };\n }\n\n const frontmatter = cloneFrontmatter(input.frontmatter);\n\n return {\n body,\n frontmatter,\n raw: stringifyNoteDocument({\n body,\n frontmatter,\n }),\n };\n}\n\ninterface ParsedFrontmatterBlock {\n body: string;\n frontmatterRaw: string;\n}\n\nfunction normalizeLineEndings(value: string): string {\n return value.replace(/\\r\\n?/gu, \"\\n\");\n}\n\nfunction cloneFrontmatter<TFrontmatter extends NoteFrontmatter>(\n frontmatter: TFrontmatter,\n): TFrontmatter {\n return JSON.parse(JSON.stringify(frontmatter)) as TFrontmatter;\n}\n\nfunction parseFrontmatterBlock(raw: string): ParsedFrontmatterBlock | null {\n if (!raw.startsWith(\"---\")) {\n return null;\n }\n\n const openingMatch = raw.match(/^---(?:\\n|$)/u);\n\n if (!openingMatch) {\n return null;\n }\n\n const openLength = openingMatch[0].length;\n const closingMatcher = /(?:^|\\n)(?:---|\\.\\.\\.)(?:\\n|$)/gu;\n closingMatcher.lastIndex = openLength;\n const closingMatch = closingMatcher.exec(raw);\n\n if (!closingMatch) {\n return null;\n }\n\n const closingStart = closingMatch.index + (closingMatch[0].startsWith(\"\\n\") ? 1 : 0);\n const closingLength = closingMatch[0].startsWith(\"\\n\")\n ? closingMatch[0].length - 1\n : closingMatch[0].length;\n const frontmatterRaw = raw.slice(openLength, closingStart);\n const bodyStart = closingStart + closingLength;\n\n return {\n body: raw.slice(bodyStart),\n frontmatterRaw: frontmatterRaw.trimEnd(),\n };\n}\n"],"mappings":";;AAIA,SAAgB,kBAAkB,KAA2B;CAC3D,MAAM,gBAAgB,qBAAqB,IAAI;CAC/C,MAAM,SAAS,sBAAsB,cAAc;AAEnD,KAAI,CAAC,OACH,QAAO;EACL,MAAM;EACN,aAAa;EACb,KAAK;EACN;CAGH,MAAM,oBAAoBA,MAAU,OAAO,eAAe;AAE1D,KACE,sBAAsB,SACrB,OAAO,sBAAsB,YAAY,MAAM,QAAQ,kBAAkB,EAE1E,OAAM,IAAI,MAAM,yDAAyD;AAG3E,QAAO;EACL,MAAM,qBAAqB,OAAO,KAAK;EACvC,aAAc,qBAAqB,EAAE;EACrC,KAAK;EACN;;AAGH,SAAgB,sBAAsB,OAA0B;CAC9D,MAAM,iBAAiB,qBAAqB,MAAM,QAAQ,GAAG;AAE7D,KAAI,CAAC,MAAM,YACT,QAAO;CAGT,MAAM,kBAAkBC,UAAc,MAAM,YAAY,CAAC,SAAS;AAGlE,QAAO,GAFoB,kBAAkB,QAAQ,gBAAgB,WAAW,eAEjD;;AAGjC,SAAgB,mBAAmB,OAAgC;CACjE,MAAM,OAAO,qBAAqB,MAAM,QAAQ,GAAG;AAEnD,KAAI,MAAM,gBAAgB,KAAA,KAAa,MAAM,gBAAgB,KAC3D,QAAO;EACL;EACA,aAAa;EACb,KAAK,sBAAsB,EAAE,MAAM,CAAC;EACrC;CAGH,MAAM,cAAc,iBAAiB,MAAM,YAAY;AAEvD,QAAO;EACL;EACA;EACA,KAAK,sBAAsB;GACzB;GACA;GACD,CAAC;EACH;;AAQH,SAAS,qBAAqB,OAAuB;AACnD,QAAO,MAAM,QAAQ,WAAW,KAAK;;AAGvC,SAAS,iBACP,aACc;AACd,QAAO,KAAK,MAAM,KAAK,UAAU,YAAY,CAAC;;AAGhD,SAAS,sBAAsB,KAA4C;AACzE,KAAI,CAAC,IAAI,WAAW,MAAM,CACxB,QAAO;CAGT,MAAM,eAAe,IAAI,MAAM,gBAAgB;AAE/C,KAAI,CAAC,aACH,QAAO;CAGT,MAAM,aAAa,aAAa,GAAG;CACnC,MAAM,iBAAiB;AACvB,gBAAe,YAAY;CAC3B,MAAM,eAAe,eAAe,KAAK,IAAI;AAE7C,KAAI,CAAC,aACH,QAAO;CAGT,MAAM,eAAe,aAAa,SAAS,aAAa,GAAG,WAAW,KAAK,GAAG,IAAI;CAClF,MAAM,gBAAgB,aAAa,GAAG,WAAW,KAAK,GAClD,aAAa,GAAG,SAAS,IACzB,aAAa,GAAG;CACpB,MAAM,iBAAiB,IAAI,MAAM,YAAY,aAAa;CAC1D,MAAM,YAAY,eAAe;AAEjC,QAAO;EACL,MAAM,IAAI,MAAM,UAAU;EAC1B,gBAAgB,eAAe,SAAS;EACzC"}
package/dist/index.d.mts CHANGED
@@ -1,8 +1,22 @@
1
- import { $ as WaitForOptions, A as ExecOptions, B as OpenTabOptions, C as FailureArtifactTask, D as CreateObsidianClientOptions, E as CommandTransport, F as ObsidianArg, G as PluginWaitUntilReadyOptions, H as PluginHandle, I as ObsidianClient, J as TabsOptions, K as RestartAppOptions, L as ObsidianCommandHandle, M as JsonFile, N as JsonFileUpdater, O as DevDomQueryOptions, P as ObsidianAppHandle, Q as VaultWriteOptions, R as ObsidianDevHandle, S as FailureArtifactRegistrationOptions, T as CommandListOptions, U as PluginReloadOptions, V as PluginDataPredicate, W as PluginWaitForDataOptions, X as VaultContentPredicate, Y as VaultApi, Z as VaultWaitForContentOptions, a as acquireVaultRunLock, b as FailureArtifactConfig, c as readVaultRunLockMarker, et as WorkspaceNode, i as VaultRunLockState, j as ExecResult, k as DevDomResult, n as VaultRunLock, nt as WorkspaceTab, o as clearVaultRunLockMarker, q as SandboxApi, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, tt as WorkspaceOptions, v as CaptureFailureArtifactsOptions, w as captureFailureArtifacts, x as FailureArtifactOptions, y as DEFAULT_FAILURE_ARTIFACTS_DIR, z as OpenFileOptions } from "./vault-lock-LmqAsLDT.mjs";
1
+ import { A as PluginHandle, B as VaultApi, C as ObsidianClient, D as OpenFileOptions, E as ObsidianMetadataHandle, F as PluginWaitUntilReadyOptions, G as WorkspaceNode, H as VaultWaitForContentOptions, I as PluginWithPatchedDataOptions, K as WorkspaceOptions, L as RestartAppOptions, N as PluginUpdateDataOptions, O as OpenTabOptions, P as PluginWaitForDataOptions, R as SandboxApi, S as ObsidianArg, T as ObsidianDevHandle, U as VaultWriteOptions, V as VaultContentPredicate, W as WaitForOptions, _ as NoteDocument, a as DevDiagnostics, b as NoteMatcherOptions, c as DevNoticeEvent, d as ExecResult, f as JsonFile, g as MetadataWaitOptions, h as MetadataPredicate, i as DevConsoleMessage, j as PluginReloadOptions, k as PluginDataPredicate, l as DevRuntimeError, m as MetadataFileCache, n as CommandTransport, o as DevDomQueryOptions, p as JsonFileUpdater, q as WorkspaceTab, r as CreateObsidianClientOptions, s as DevDomResult, t as CommandListOptions, u as ExecOptions, v as NoteFrontmatter, w as ObsidianCommandHandle, x as ObsidianAppHandle, y as NoteInput, z as TabsOptions } from "./types-BUXaueDI.mjs";
2
+ import { C as FailureArtifactConfig, D as captureFailureArtifacts, E as FailureArtifactTask, S as DEFAULT_FAILURE_ARTIFACTS_DIR, T as FailureArtifactRegistrationOptions, _ as TestContext, a as acquireVaultRunLock, b as VaultSeedEntry, c as readVaultRunLockMarker, d as ObsidianFixtures, f as ObsidianTest, g as SharedVaultLockOptions, h as PluginTest, i as VaultRunLockState, l as CreateObsidianTestOptions, m as PluginSessionOptions, n as VaultRunLock, o as clearVaultRunLockMarker, p as PluginFixtures, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, u as CreatePluginTestOptions, v as TestContextCleanupOptions, w as FailureArtifactOptions, x as CaptureFailureArtifactsOptions, y as VaultSeed } from "./vault-lock-XcBHtwm9.mjs";
2
3
 
3
4
  //#region src/core/client.d.ts
4
5
  declare function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient;
5
6
  //#endregion
7
+ //#region src/fixtures/test-context.d.ts
8
+ declare function createTestContext(options: CreateObsidianTestOptions & {
9
+ testName?: string;
10
+ }): Promise<TestContext>;
11
+ declare function withVaultSandbox<TResult>(options: CreateObsidianTestOptions & {
12
+ testName?: string;
13
+ }, run: (context: TestContext) => Promise<TResult> | TResult): Promise<TResult>;
14
+ //#endregion
15
+ //#region src/note/document.d.ts
16
+ declare function parseNoteDocument(raw: string): NoteDocument;
17
+ declare function stringifyNoteDocument(input: NoteInput): string;
18
+ declare function createNoteDocument(input: NoteInput): NoteDocument;
19
+ //#endregion
6
20
  //#region src/vault/sandbox.d.ts
7
21
  interface CreateSandboxApiOptions {
8
22
  obsidian: ObsidianClient;
@@ -18,5 +32,5 @@ interface CreateVaultApiOptions {
18
32
  }
19
33
  declare function createVaultApi(options: CreateVaultApiOptions): VaultApi;
20
34
  //#endregion
21
- export { type AcquireVaultRunLockOptions, type CaptureFailureArtifactsOptions, type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, DEFAULT_FAILURE_ARTIFACTS_DIR, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type FailureArtifactConfig, type FailureArtifactOptions, type FailureArtifactRegistrationOptions, type FailureArtifactTask, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginDataPredicate, type PluginHandle, type PluginReloadOptions, type PluginWaitForDataOptions, type PluginWaitUntilReadyOptions, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type VaultContentPredicate, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type VaultWaitForContentOptions, type VaultWriteOptions, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
35
+ export { type AcquireVaultRunLockOptions, type CaptureFailureArtifactsOptions, type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, type CreateObsidianTestOptions, type CreatePluginTestOptions, DEFAULT_FAILURE_ARTIFACTS_DIR, type DevConsoleMessage, type DevDiagnostics, type DevDomQueryOptions, type DevDomResult, type DevNoticeEvent, type DevRuntimeError, type ExecOptions, type ExecResult, type FailureArtifactConfig, type FailureArtifactOptions, type FailureArtifactRegistrationOptions, type FailureArtifactTask, type JsonFile, type JsonFileUpdater, type MetadataFileCache, type MetadataPredicate, type MetadataWaitOptions, type NoteDocument, type NoteFrontmatter, type NoteInput, type NoteMatcherOptions, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type ObsidianFixtures, type ObsidianMetadataHandle, type ObsidianTest, type OpenFileOptions, type OpenTabOptions, type PluginDataPredicate, type PluginFixtures, type PluginHandle, type PluginReloadOptions, type PluginSessionOptions, type PluginTest, type PluginUpdateDataOptions, type PluginWaitForDataOptions, type PluginWaitUntilReadyOptions, type PluginWithPatchedDataOptions, type RestartAppOptions, type SandboxApi, type SharedVaultLockOptions, type TabsOptions, type TestContext, type TestContextCleanupOptions, type VaultApi, type VaultContentPredicate, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type VaultSeed, type VaultSeedEntry, type VaultWaitForContentOptions, type VaultWriteOptions, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createNoteDocument, createObsidianClient, createSandboxApi, createTestContext, createVaultApi, inspectVaultRunLock, parseNoteDocument, readVaultRunLockMarker, stringifyNoteDocument, withVaultSandbox };
22
36
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import { a as clearVaultRunLockMarker, c as DEFAULT_FAILURE_ARTIFACTS_DIR, d as createObsidianClient, i as acquireVaultRunLock, l as captureFailureArtifacts, n as createVaultApi, o as inspectVaultRunLock, s as readVaultRunLockMarker, t as createSandboxApi } from "./sandbox--mUbNsh7.mjs";
2
- export { DEFAULT_FAILURE_ARTIFACTS_DIR, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
1
+ import { a as clearVaultRunLockMarker, c as createSandboxApi, d as createVaultApi, i as acquireVaultRunLock, l as DEFAULT_FAILURE_ARTIFACTS_DIR, n as withVaultSandbox, o as inspectVaultRunLock, p as createObsidianClient, s as readVaultRunLockMarker, t as createTestContext, u as captureFailureArtifacts } from "./test-context-Bl-e-83H.mjs";
2
+ import { n as parseNoteDocument, r as stringifyNoteDocument, t as createNoteDocument } from "./document-DunL2Moz.mjs";
3
+ export { DEFAULT_FAILURE_ARTIFACTS_DIR, acquireVaultRunLock, captureFailureArtifacts, clearVaultRunLockMarker, createNoteDocument, createObsidianClient, createSandboxApi, createTestContext, createVaultApi, inspectVaultRunLock, parseNoteDocument, readVaultRunLockMarker, stringifyNoteDocument, withVaultSandbox };
@@ -1,3 +1,5 @@
1
+ import { b as NoteMatcherOptions, v as NoteFrontmatter } from "./types-BUXaueDI.mjs";
2
+
1
3
  //#region src/matchers.d.ts
2
4
  declare module "vite-plus/test" {
3
5
  interface Assertion<T = any> {
@@ -6,7 +8,9 @@ declare module "vite-plus/test" {
6
8
  toHaveEditorTextContaining(needle: string): Promise<T>;
7
9
  toHaveFile(path: string): Promise<T>;
8
10
  toHaveFileContaining(path: string, needle: string): Promise<T>;
11
+ toHaveFrontmatter(path: string, expected: NoteFrontmatter): Promise<T>;
9
12
  toHaveJsonFile(path: string): Promise<T>;
13
+ toHaveNote(path: string, expected: NoteMatcherOptions): Promise<T>;
10
14
  toHaveOpenTab(title: string, viewType?: string): Promise<T>;
11
15
  toHavePluginData(expected: unknown): Promise<T>;
12
16
  toHaveWorkspaceNode(label: string): Promise<T>;
@@ -17,7 +21,9 @@ declare module "vite-plus/test" {
17
21
  toHaveEditorTextContaining(needle: string): void;
18
22
  toHaveFile(path: string): void;
19
23
  toHaveFileContaining(path: string, needle: string): void;
24
+ toHaveFrontmatter(path: string, expected: NoteFrontmatter): void;
20
25
  toHaveJsonFile(path: string): void;
26
+ toHaveNote(path: string, expected: NoteMatcherOptions): void;
21
27
  toHaveOpenTab(title: string, viewType?: string): void;
22
28
  toHavePluginData(expected: unknown): void;
23
29
  toHaveWorkspaceNode(label: string): void;
package/dist/matchers.mjs CHANGED
@@ -1,9 +1,10 @@
1
+ import { n as parseNoteDocument } from "./document-DunL2Moz.mjs";
1
2
  import { expect } from "vite-plus/test";
2
3
  import { isDeepStrictEqual } from "node:util";
3
4
  //#region src/matchers.ts
4
5
  expect.extend({
5
6
  async toHaveActiveFile(target, targetPath) {
6
- const actual = await target.dev.eval("app.workspace.getActiveFile()?.path ?? null");
7
+ const actual = await target.dev.activeFilePath();
7
8
  const pass = actual === targetPath;
8
9
  return {
9
10
  message: () => pass ? `Expected active file not to be "${targetPath}"` : `Expected active file to be "${targetPath}", received ${JSON.stringify(actual)}`,
@@ -35,6 +36,18 @@ expect.extend({
35
36
  pass
36
37
  };
37
38
  },
39
+ async toHaveFrontmatter(target, targetPath, expected) {
40
+ if (!await target.exists(targetPath)) return {
41
+ message: () => `Expected vault path to exist: ${targetPath}`,
42
+ pass: false
43
+ };
44
+ const actual = parseNoteDocument(await target.read(targetPath)).frontmatter;
45
+ const pass = isDeepStrictEqual(actual, expected);
46
+ return {
47
+ message: () => pass ? `Expected frontmatter for "${targetPath}" not to equal ${JSON.stringify(expected)}` : `Expected frontmatter for "${targetPath}" to equal ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`,
48
+ pass
49
+ };
50
+ },
38
51
  async toHaveJsonFile(target, targetPath) {
39
52
  if (!await target.exists(targetPath)) return {
40
53
  message: () => `Expected JSON file to exist: ${targetPath}`,
@@ -53,6 +66,25 @@ expect.extend({
53
66
  };
54
67
  }
55
68
  },
69
+ async toHaveNote(target, targetPath, expected) {
70
+ let actual;
71
+ try {
72
+ actual = parseNoteDocument(await target.read(targetPath));
73
+ } catch (error) {
74
+ return {
75
+ message: () => `Expected vault path "${targetPath}" to be readable as a note, but reading failed: ${error instanceof Error ? error.message : String(error)}`,
76
+ pass: false
77
+ };
78
+ }
79
+ const matchesFrontmatter = expected.frontmatter === void 0 || isDeepStrictEqual(actual.frontmatter, expected.frontmatter);
80
+ const matchesBody = expected.body === void 0 || actual.body === expected.body;
81
+ const matchesBodyIncludes = expected.bodyIncludes === void 0 || actual.body.includes(expected.bodyIncludes);
82
+ const pass = matchesFrontmatter && matchesBody && matchesBodyIncludes;
83
+ return {
84
+ message: () => pass ? `Expected vault path "${targetPath}" not to match the expected note shape` : `Expected vault path "${targetPath}" to match the expected note shape, received ${JSON.stringify(actual)}`,
85
+ pass
86
+ };
87
+ },
56
88
  async toHaveOpenTab(target, title, viewType) {
57
89
  const pass = (await target.tabs()).some((tab) => tab.title === title && (viewType === void 0 || tab.viewType === viewType));
58
90
  return {
@@ -69,7 +101,7 @@ expect.extend({
69
101
  };
70
102
  },
71
103
  async toHaveEditorTextContaining(target, needle) {
72
- const actual = await target.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null");
104
+ const actual = await target.dev.editorText();
73
105
  const pass = typeof actual === "string" && actual.includes(needle);
74
106
  return {
75
107
  message: () => pass ? `Expected editor text not to contain "${needle}"` : `Expected editor text to contain "${needle}", received ${JSON.stringify(actual)}`,
@@ -1 +1 @@
1
- {"version":3,"file":"matchers.mjs","names":[],"sources":["../src/matchers.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\n\nimport { expect } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle, SandboxApi, VaultApi } from \"./core/types\";\n\ntype FileMatcherTarget = SandboxApi | VaultApi;\n\nexpect.extend({\n async toHaveActiveFile(target: ObsidianClient, targetPath: string) {\n const actual = await target.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n );\n const pass = actual === targetPath;\n\n return {\n message: () =>\n pass\n ? `Expected active file not to be \"${targetPath}\"`\n : `Expected active file to be \"${targetPath}\", received ${JSON.stringify(actual)}`,\n pass,\n };\n },\n async toHaveCommand(target: ObsidianClient, commandId: string) {\n const pass = await target.command(commandId).exists();\n\n return {\n message: () =>\n pass\n ? `Expected Obsidian command not to exist: ${commandId}`\n : `Expected Obsidian command to exist: ${commandId}`,\n pass,\n };\n },\n async toHaveFile(target: FileMatcherTarget, targetPath: string) {\n const pass = await target.exists(targetPath);\n\n return {\n message: () =>\n pass\n ? `Expected vault path not to exist: ${targetPath}`\n : `Expected vault path to exist: ${targetPath}`,\n pass,\n };\n },\n async toHaveFileContaining(target: FileMatcherTarget, targetPath: string, needle: string) {\n const exists = await target.exists(targetPath);\n\n if (!exists) {\n return {\n message: () => `Expected vault path to exist: ${targetPath}`,\n pass: false,\n };\n }\n\n const content = await target.read(targetPath);\n const pass = content.includes(needle);\n\n return {\n message: () =>\n pass\n ? `Expected vault path \"${targetPath}\" not to contain \"${needle}\"`\n : `Expected vault path \"${targetPath}\" to contain \"${needle}\"`,\n pass,\n };\n },\n async toHaveJsonFile(target: FileMatcherTarget, targetPath: string) {\n const exists = await target.exists(targetPath);\n\n if (!exists) {\n return {\n message: () => `Expected JSON file to exist: ${targetPath}`,\n pass: false,\n };\n }\n\n try {\n await target.json(targetPath).read();\n return {\n message: () => `Expected JSON file \"${targetPath}\" not to be valid JSON`,\n pass: true,\n };\n } catch (error) {\n return {\n message: () =>\n `Expected JSON file \"${targetPath}\" to be valid JSON, but parsing failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n pass: false,\n };\n }\n },\n async toHaveOpenTab(target: ObsidianClient, title: string, viewType?: string) {\n const tabs = await target.tabs();\n const pass = tabs.some(\n (tab) => tab.title === title && (viewType === undefined || tab.viewType === viewType),\n );\n\n return {\n message: () =>\n pass\n ? `Expected no open tab matching \"${title}\"${\n viewType ? ` with view type \"${viewType}\"` : \"\"\n }`\n : `Expected an open tab matching \"${title}\"${\n viewType ? ` with view type \"${viewType}\"` : \"\"\n }`,\n pass,\n };\n },\n async toHavePluginData(target: PluginHandle, expected: unknown) {\n const actual = await target.data().read();\n const pass = isDeepStrictEqual(actual, expected);\n\n return {\n message: () =>\n pass\n ? `Expected plugin data not to equal ${JSON.stringify(expected)}`\n : `Expected plugin data to equal ${JSON.stringify(expected)}, received ${JSON.stringify(\n actual,\n )}`,\n pass,\n };\n },\n async toHaveEditorTextContaining(target: ObsidianClient, needle: string) {\n const actual = await target.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n );\n const pass = typeof actual === \"string\" && actual.includes(needle);\n\n return {\n message: () =>\n pass\n ? `Expected editor text not to contain \"${needle}\"`\n : `Expected editor text to contain \"${needle}\", received ${JSON.stringify(actual)}`,\n pass,\n };\n },\n async toHaveWorkspaceNode(target: ObsidianClient, label: string) {\n const pass = hasWorkspaceNode(await target.workspace(), label);\n\n return {\n message: () =>\n pass\n ? `Expected workspace not to contain node \"${label}\"`\n : `Expected workspace to contain node \"${label}\"`,\n pass,\n };\n },\n});\n\ndeclare module \"vite-plus/test\" {\n interface Assertion<T = any> {\n toHaveActiveFile(path: string): Promise<T>;\n toHaveCommand(commandId: string): Promise<T>;\n toHaveEditorTextContaining(needle: string): Promise<T>;\n toHaveFile(path: string): Promise<T>;\n toHaveFileContaining(path: string, needle: string): Promise<T>;\n toHaveJsonFile(path: string): Promise<T>;\n toHaveOpenTab(title: string, viewType?: string): Promise<T>;\n toHavePluginData(expected: unknown): Promise<T>;\n toHaveWorkspaceNode(label: string): Promise<T>;\n }\n\n interface AsymmetricMatchersContaining {\n toHaveActiveFile(path: string): void;\n toHaveCommand(commandId: string): void;\n toHaveEditorTextContaining(needle: string): void;\n toHaveFile(path: string): void;\n toHaveFileContaining(path: string, needle: string): void;\n toHaveJsonFile(path: string): void;\n toHaveOpenTab(title: string, viewType?: string): void;\n toHavePluginData(expected: unknown): void;\n toHaveWorkspaceNode(label: string): void;\n }\n}\n\nexport {};\n\nfunction hasWorkspaceNode(\n nodes: Awaited<ReturnType<ObsidianClient[\"workspace\"]>>,\n label: string,\n): boolean {\n for (const node of nodes) {\n if (node.label === label || hasWorkspaceNode(node.children, label)) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":";;;AAQA,OAAO,OAAO;CACZ,MAAM,iBAAiB,QAAwB,YAAoB;EACjE,MAAM,SAAS,MAAM,OAAO,IAAI,KAC9B,8CACD;EACD,MAAM,OAAO,WAAW;AAExB,SAAO;GACL,eACE,OACI,mCAAmC,WAAW,KAC9C,+BAA+B,WAAW,cAAc,KAAK,UAAU,OAAO;GACpF;GACD;;CAEH,MAAM,cAAc,QAAwB,WAAmB;EAC7D,MAAM,OAAO,MAAM,OAAO,QAAQ,UAAU,CAAC,QAAQ;AAErD,SAAO;GACL,eACE,OACI,2CAA2C,cAC3C,uCAAuC;GAC7C;GACD;;CAEH,MAAM,WAAW,QAA2B,YAAoB;EAC9D,MAAM,OAAO,MAAM,OAAO,OAAO,WAAW;AAE5C,SAAO;GACL,eACE,OACI,qCAAqC,eACrC,iCAAiC;GACvC;GACD;;CAEH,MAAM,qBAAqB,QAA2B,YAAoB,QAAgB;AAGxF,MAAI,CAFW,MAAM,OAAO,OAAO,WAAW,CAG5C,QAAO;GACL,eAAe,iCAAiC;GAChD,MAAM;GACP;EAIH,MAAM,QADU,MAAM,OAAO,KAAK,WAAW,EACxB,SAAS,OAAO;AAErC,SAAO;GACL,eACE,OACI,wBAAwB,WAAW,oBAAoB,OAAO,KAC9D,wBAAwB,WAAW,gBAAgB,OAAO;GAChE;GACD;;CAEH,MAAM,eAAe,QAA2B,YAAoB;AAGlE,MAAI,CAFW,MAAM,OAAO,OAAO,WAAW,CAG5C,QAAO;GACL,eAAe,gCAAgC;GAC/C,MAAM;GACP;AAGH,MAAI;AACF,SAAM,OAAO,KAAK,WAAW,CAAC,MAAM;AACpC,UAAO;IACL,eAAe,uBAAuB,WAAW;IACjD,MAAM;IACP;WACM,OAAO;AACd,UAAO;IACL,eACE,uBAAuB,WAAW,0CAChC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;IAE1D,MAAM;IACP;;;CAGL,MAAM,cAAc,QAAwB,OAAe,UAAmB;EAE5E,MAAM,QADO,MAAM,OAAO,MAAM,EACd,MACf,QAAQ,IAAI,UAAU,UAAU,aAAa,KAAA,KAAa,IAAI,aAAa,UAC7E;AAED,SAAO;GACL,eACE,OACI,kCAAkC,MAAM,GACtC,WAAW,oBAAoB,SAAS,KAAK,OAE/C,kCAAkC,MAAM,GACtC,WAAW,oBAAoB,SAAS,KAAK;GAErD;GACD;;CAEH,MAAM,iBAAiB,QAAsB,UAAmB;EAC9D,MAAM,SAAS,MAAM,OAAO,MAAM,CAAC,MAAM;EACzC,MAAM,OAAO,kBAAkB,QAAQ,SAAS;AAEhD,SAAO;GACL,eACE,OACI,qCAAqC,KAAK,UAAU,SAAS,KAC7D,iCAAiC,KAAK,UAAU,SAAS,CAAC,aAAa,KAAK,UAC1E,OACD;GACP;GACD;;CAEH,MAAM,2BAA2B,QAAwB,QAAgB;EACvE,MAAM,SAAS,MAAM,OAAO,IAAI,KAC9B,+DACD;EACD,MAAM,OAAO,OAAO,WAAW,YAAY,OAAO,SAAS,OAAO;AAElE,SAAO;GACL,eACE,OACI,wCAAwC,OAAO,KAC/C,oCAAoC,OAAO,cAAc,KAAK,UAAU,OAAO;GACrF;GACD;;CAEH,MAAM,oBAAoB,QAAwB,OAAe;EAC/D,MAAM,OAAO,iBAAiB,MAAM,OAAO,WAAW,EAAE,MAAM;AAE9D,SAAO;GACL,eACE,OACI,2CAA2C,MAAM,KACjD,uCAAuC,MAAM;GACnD;GACD;;CAEJ,CAAC;AA8BF,SAAS,iBACP,OACA,OACS;AACT,MAAK,MAAM,QAAQ,MACjB,KAAI,KAAK,UAAU,SAAS,iBAAiB,KAAK,UAAU,MAAM,CAChE,QAAO;AAIX,QAAO"}
1
+ {"version":3,"file":"matchers.mjs","names":[],"sources":["../src/matchers.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\n\nimport { expect } from \"vite-plus/test\";\n\nimport { parseNoteDocument } from \"./note/document\";\nimport type {\n NoteMatcherOptions,\n NoteFrontmatter,\n ObsidianClient,\n PluginHandle,\n SandboxApi,\n VaultApi,\n} from \"./core/types\";\n\ntype FileMatcherTarget = SandboxApi | VaultApi;\n\nexpect.extend({\n async toHaveActiveFile(target: ObsidianClient, targetPath: string) {\n const actual = await target.dev.activeFilePath();\n const pass = actual === targetPath;\n\n return {\n message: () =>\n pass\n ? `Expected active file not to be \"${targetPath}\"`\n : `Expected active file to be \"${targetPath}\", received ${JSON.stringify(actual)}`,\n pass,\n };\n },\n async toHaveCommand(target: ObsidianClient, commandId: string) {\n const pass = await target.command(commandId).exists();\n\n return {\n message: () =>\n pass\n ? `Expected Obsidian command not to exist: ${commandId}`\n : `Expected Obsidian command to exist: ${commandId}`,\n pass,\n };\n },\n async toHaveFile(target: FileMatcherTarget, targetPath: string) {\n const pass = await target.exists(targetPath);\n\n return {\n message: () =>\n pass\n ? `Expected vault path not to exist: ${targetPath}`\n : `Expected vault path to exist: ${targetPath}`,\n pass,\n };\n },\n async toHaveFileContaining(target: FileMatcherTarget, targetPath: string, needle: string) {\n const exists = await target.exists(targetPath);\n\n if (!exists) {\n return {\n message: () => `Expected vault path to exist: ${targetPath}`,\n pass: false,\n };\n }\n\n const content = await target.read(targetPath);\n const pass = content.includes(needle);\n\n return {\n message: () =>\n pass\n ? `Expected vault path \"${targetPath}\" not to contain \"${needle}\"`\n : `Expected vault path \"${targetPath}\" to contain \"${needle}\"`,\n pass,\n };\n },\n async toHaveFrontmatter(\n target: FileMatcherTarget,\n targetPath: string,\n expected: NoteFrontmatter,\n ) {\n const exists = await target.exists(targetPath);\n\n if (!exists) {\n return {\n message: () => `Expected vault path to exist: ${targetPath}`,\n pass: false,\n };\n }\n\n const actual = parseNoteDocument(await target.read(targetPath)).frontmatter;\n const pass = isDeepStrictEqual(actual, expected);\n\n return {\n message: () =>\n pass\n ? `Expected frontmatter for \"${targetPath}\" not to equal ${JSON.stringify(expected)}`\n : `Expected frontmatter for \"${targetPath}\" to equal ${JSON.stringify(\n expected,\n )}, received ${JSON.stringify(actual)}`,\n pass,\n };\n },\n async toHaveJsonFile(target: FileMatcherTarget, targetPath: string) {\n const exists = await target.exists(targetPath);\n\n if (!exists) {\n return {\n message: () => `Expected JSON file to exist: ${targetPath}`,\n pass: false,\n };\n }\n\n try {\n await target.json(targetPath).read();\n return {\n message: () => `Expected JSON file \"${targetPath}\" not to be valid JSON`,\n pass: true,\n };\n } catch (error) {\n return {\n message: () =>\n `Expected JSON file \"${targetPath}\" to be valid JSON, but parsing failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n pass: false,\n };\n }\n },\n async toHaveNote(target: FileMatcherTarget, targetPath: string, expected: NoteMatcherOptions) {\n let actual: ReturnType<typeof parseNoteDocument>;\n\n try {\n actual = parseNoteDocument(await target.read(targetPath));\n } catch (error) {\n return {\n message: () =>\n `Expected vault path \"${targetPath}\" to be readable as a note, but reading failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n pass: false,\n };\n }\n\n const matchesFrontmatter =\n expected.frontmatter === undefined ||\n isDeepStrictEqual(actual.frontmatter, expected.frontmatter);\n const matchesBody = expected.body === undefined || actual.body === expected.body;\n const matchesBodyIncludes =\n expected.bodyIncludes === undefined || actual.body.includes(expected.bodyIncludes);\n const pass = matchesFrontmatter && matchesBody && matchesBodyIncludes;\n\n return {\n message: () =>\n pass\n ? `Expected vault path \"${targetPath}\" not to match the expected note shape`\n : `Expected vault path \"${targetPath}\" to match the expected note shape, received ${JSON.stringify(\n actual,\n )}`,\n pass,\n };\n },\n async toHaveOpenTab(target: ObsidianClient, title: string, viewType?: string) {\n const tabs = await target.tabs();\n const pass = tabs.some(\n (tab) => tab.title === title && (viewType === undefined || tab.viewType === viewType),\n );\n\n return {\n message: () =>\n pass\n ? `Expected no open tab matching \"${title}\"${\n viewType ? ` with view type \"${viewType}\"` : \"\"\n }`\n : `Expected an open tab matching \"${title}\"${\n viewType ? ` with view type \"${viewType}\"` : \"\"\n }`,\n pass,\n };\n },\n async toHavePluginData(target: PluginHandle, expected: unknown) {\n const actual = await target.data().read();\n const pass = isDeepStrictEqual(actual, expected);\n\n return {\n message: () =>\n pass\n ? `Expected plugin data not to equal ${JSON.stringify(expected)}`\n : `Expected plugin data to equal ${JSON.stringify(expected)}, received ${JSON.stringify(\n actual,\n )}`,\n pass,\n };\n },\n async toHaveEditorTextContaining(target: ObsidianClient, needle: string) {\n const actual = await target.dev.editorText();\n const pass = typeof actual === \"string\" && actual.includes(needle);\n\n return {\n message: () =>\n pass\n ? `Expected editor text not to contain \"${needle}\"`\n : `Expected editor text to contain \"${needle}\", received ${JSON.stringify(actual)}`,\n pass,\n };\n },\n async toHaveWorkspaceNode(target: ObsidianClient, label: string) {\n const pass = hasWorkspaceNode(await target.workspace(), label);\n\n return {\n message: () =>\n pass\n ? `Expected workspace not to contain node \"${label}\"`\n : `Expected workspace to contain node \"${label}\"`,\n pass,\n };\n },\n});\n\ndeclare module \"vite-plus/test\" {\n interface Assertion<T = any> {\n toHaveActiveFile(path: string): Promise<T>;\n toHaveCommand(commandId: string): Promise<T>;\n toHaveEditorTextContaining(needle: string): Promise<T>;\n toHaveFile(path: string): Promise<T>;\n toHaveFileContaining(path: string, needle: string): Promise<T>;\n toHaveFrontmatter(path: string, expected: NoteFrontmatter): Promise<T>;\n toHaveJsonFile(path: string): Promise<T>;\n toHaveNote(path: string, expected: NoteMatcherOptions): Promise<T>;\n toHaveOpenTab(title: string, viewType?: string): Promise<T>;\n toHavePluginData(expected: unknown): Promise<T>;\n toHaveWorkspaceNode(label: string): Promise<T>;\n }\n\n interface AsymmetricMatchersContaining {\n toHaveActiveFile(path: string): void;\n toHaveCommand(commandId: string): void;\n toHaveEditorTextContaining(needle: string): void;\n toHaveFile(path: string): void;\n toHaveFileContaining(path: string, needle: string): void;\n toHaveFrontmatter(path: string, expected: NoteFrontmatter): void;\n toHaveJsonFile(path: string): void;\n toHaveNote(path: string, expected: NoteMatcherOptions): void;\n toHaveOpenTab(title: string, viewType?: string): void;\n toHavePluginData(expected: unknown): void;\n toHaveWorkspaceNode(label: string): void;\n }\n}\n\nexport {};\n\nfunction hasWorkspaceNode(\n nodes: Awaited<ReturnType<ObsidianClient[\"workspace\"]>>,\n label: string,\n): boolean {\n for (const node of nodes) {\n if (node.label === label || hasWorkspaceNode(node.children, label)) {\n return true;\n }\n }\n\n return false;\n}\n"],"mappings":"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"}