obsidian-e2e 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -8
- package/dist/document-DunL2Moz.mjs +69 -0
- package/dist/document-DunL2Moz.mjs.map +1 -0
- package/dist/index.d.mts +16 -2
- package/dist/index.mjs +3 -2
- package/dist/matchers.d.mts +6 -0
- package/dist/matchers.mjs +30 -2
- package/dist/matchers.mjs.map +1 -1
- package/dist/test-context-BprSx6U1.mjs +1558 -0
- package/dist/test-context-BprSx6U1.mjs.map +1 -0
- package/dist/types-C4cj443K.d.mts +256 -0
- package/dist/vault-lock-CYyOdRP1.d.mts +136 -0
- package/dist/vitest.d.mts +1 -1
- package/dist/vitest.mjs +17 -176
- package/dist/vitest.mjs.map +1 -1
- package/package.json +4 -1
- package/dist/sandbox-Cz3rj_Rn.mjs +0 -728
- package/dist/sandbox-Cz3rj_Rn.mjs.map +0 -1
- package/dist/vault-lock-DarzOEzv.d.mts +0 -242
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.
|
|
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,70 @@ 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()`, `frontmatter()`,
|
|
241
|
+
`waitForFrontmatter()`, and `waitForMetadata()`
|
|
234
242
|
|
|
235
243
|
Plugin data mutations are snapshotted on first write and restored automatically
|
|
236
244
|
after each test. Sandbox files are also cleaned up automatically.
|
|
237
245
|
|
|
246
|
+
## Note Helpers And Test Context
|
|
247
|
+
|
|
248
|
+
Use `sandbox.writeNote()` when the test cares about note structure rather than
|
|
249
|
+
raw YAML formatting:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
await sandbox.writeNote({
|
|
253
|
+
path: "Inbox/Today.md",
|
|
254
|
+
frontmatter: {
|
|
255
|
+
mood: "focused",
|
|
256
|
+
tags: ["daily"],
|
|
257
|
+
},
|
|
258
|
+
body: "# Today\n",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await expect(sandbox.readNote("Inbox/Today.md")).resolves.toMatchObject({
|
|
262
|
+
body: "# Today\n",
|
|
263
|
+
frontmatter: {
|
|
264
|
+
mood: "focused",
|
|
265
|
+
tags: ["daily"],
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await sandbox.waitForFrontmatter("Inbox/Today.md", (frontmatter) =>
|
|
270
|
+
frontmatter.tags.includes("daily"),
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`readNote()` is file-derived. `sandbox.frontmatter()` and
|
|
275
|
+
`sandbox.waitForMetadata()` are Obsidian metadata-cache reads, so tests can
|
|
276
|
+
distinguish raw file content from “Obsidian has indexed this note”.
|
|
277
|
+
|
|
278
|
+
Outside Vitest fixtures, use the public lifecycle wrapper:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { withVaultSandbox } from "obsidian-e2e";
|
|
282
|
+
|
|
283
|
+
await withVaultSandbox(
|
|
284
|
+
{
|
|
285
|
+
testName: "quickadd smoke",
|
|
286
|
+
vault: "dev",
|
|
287
|
+
},
|
|
288
|
+
async (context) => {
|
|
289
|
+
const plugin = await context.plugin("quickadd", {
|
|
290
|
+
filter: "community",
|
|
291
|
+
seedData: { enabled: true },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await context.sandbox.writeNote({
|
|
295
|
+
path: "fixtures/template.md",
|
|
296
|
+
body: "Hello from template",
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await plugin.reload();
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
238
304
|
## Plugin Test Helper
|
|
239
305
|
|
|
240
306
|
If you are testing one plugin repeatedly, `createPluginTest()` gives you a
|
|
@@ -250,7 +316,14 @@ export const test = createPluginTest({
|
|
|
250
316
|
pluginFilter: "community",
|
|
251
317
|
seedPluginData: { enabled: true },
|
|
252
318
|
seedVault: {
|
|
253
|
-
"fixtures/template.md":
|
|
319
|
+
"fixtures/template.md": {
|
|
320
|
+
note: {
|
|
321
|
+
body: "template body",
|
|
322
|
+
frontmatter: {
|
|
323
|
+
tags: ["template"],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
254
327
|
"fixtures/state.json": { json: { ready: true } },
|
|
255
328
|
},
|
|
256
329
|
});
|
|
@@ -262,6 +335,7 @@ export const test = createPluginTest({
|
|
|
262
335
|
- enables the target plugin for the test when needed and restores the prior
|
|
263
336
|
enabled/disabled state afterward
|
|
264
337
|
- seeds vault files before each test and restores the original files afterward
|
|
338
|
+
- `seedVault` accepts raw strings, `{ json }`, and `{ note }` descriptors
|
|
265
339
|
- seeds `data.json` through the normal plugin snapshot/restore path
|
|
266
340
|
- supports the same opt-in failure artifact capture as `createObsidianTest()`
|
|
267
341
|
|
|
@@ -321,8 +395,13 @@ task-id suffix, for example:
|
|
|
321
395
|
`createObsidianTest()` captures:
|
|
322
396
|
|
|
323
397
|
- `active-file.json`
|
|
398
|
+
- `active-note.md`
|
|
399
|
+
- `active-note-frontmatter.json`
|
|
400
|
+
- `console-messages.json`
|
|
324
401
|
- `dom.txt`
|
|
325
402
|
- `editor.json`
|
|
403
|
+
- `notices.json`
|
|
404
|
+
- `runtime-errors.json`
|
|
326
405
|
- `tabs.json`
|
|
327
406
|
- `workspace.json`
|
|
328
407
|
- `screenshot.png` when screenshot capture succeeds
|
|
@@ -338,6 +417,27 @@ part of the set: desktop permissions, display availability, or Obsidian state
|
|
|
338
417
|
can prevent `screenshot.png` from being produced, in which case you should
|
|
339
418
|
expect `screenshot.error.txt` instead.
|
|
340
419
|
|
|
420
|
+
If you are not using the Vitest fixtures, the same artifact capture path is
|
|
421
|
+
available directly from the main package:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { captureFailureArtifacts, createObsidianClient } from "obsidian-e2e";
|
|
425
|
+
|
|
426
|
+
const obsidian = createObsidianClient({ vault: "dev" });
|
|
427
|
+
|
|
428
|
+
await captureFailureArtifacts(
|
|
429
|
+
{
|
|
430
|
+
id: "quickadd_case_1234abcd",
|
|
431
|
+
name: "captures quickadd diagnostics",
|
|
432
|
+
},
|
|
433
|
+
obsidian,
|
|
434
|
+
{
|
|
435
|
+
captureOnFailure: true,
|
|
436
|
+
plugin: obsidian.plugin("quickadd"),
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
```
|
|
440
|
+
|
|
341
441
|
## Maintainer CI And Releases
|
|
342
442
|
|
|
343
443
|
This repo now ships with a hardened CI and release flow built around Vite+
|
|
@@ -374,7 +474,9 @@ Import `obsidian-e2e/matchers` once in your test setup to register:
|
|
|
374
474
|
- `toHaveEditorTextContaining(needle)`
|
|
375
475
|
- `toHaveFile(path)`
|
|
376
476
|
- `toHaveFileContaining(path, needle)`
|
|
477
|
+
- `toHaveFrontmatter(path, expected)`
|
|
377
478
|
- `toHaveJsonFile(path)`
|
|
479
|
+
- `toHaveNote(path, { frontmatter?, body?, bodyIncludes? })`
|
|
378
480
|
- `toHaveOpenTab(title, viewType?)`
|
|
379
481
|
- `toHavePluginData(expected)`
|
|
380
482
|
- `toHaveWorkspaceNode(label)`
|
|
@@ -386,9 +488,23 @@ import { expect } from "vite-plus/test";
|
|
|
386
488
|
import { test } from "./setup";
|
|
387
489
|
|
|
388
490
|
test("writes valid JSON into the sandbox", async ({ sandbox }) => {
|
|
389
|
-
await sandbox.
|
|
491
|
+
await sandbox.writeNote({
|
|
492
|
+
path: "Today.md",
|
|
493
|
+
frontmatter: {
|
|
494
|
+
mood: "focused",
|
|
495
|
+
},
|
|
496
|
+
body: "# Today\n",
|
|
497
|
+
});
|
|
390
498
|
|
|
391
|
-
await expect(sandbox).
|
|
499
|
+
await expect(sandbox).toHaveNote("Today.md", {
|
|
500
|
+
bodyIncludes: "Today",
|
|
501
|
+
frontmatter: {
|
|
502
|
+
mood: "focused",
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
await expect(sandbox).toHaveFrontmatter("Today.md", {
|
|
506
|
+
mood: "focused",
|
|
507
|
+
});
|
|
392
508
|
});
|
|
393
509
|
|
|
394
510
|
test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
|
|
@@ -406,15 +522,27 @@ test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
|
|
|
406
522
|
If you need to work below the fixture layer:
|
|
407
523
|
|
|
408
524
|
```ts
|
|
409
|
-
import { createObsidianClient } from "obsidian-e2e";
|
|
525
|
+
import { createObsidianClient, createVaultApi, parseNoteDocument } from "obsidian-e2e";
|
|
410
526
|
|
|
411
527
|
const obsidian = createObsidianClient({
|
|
412
528
|
vault: "dev",
|
|
413
529
|
bin: "obsidian",
|
|
530
|
+
defaultExecOptions: {
|
|
531
|
+
allowNonZeroExit: true,
|
|
532
|
+
},
|
|
414
533
|
});
|
|
534
|
+
const vault = createVaultApi({ obsidian });
|
|
415
535
|
|
|
416
536
|
await obsidian.verify();
|
|
417
|
-
await
|
|
537
|
+
await vault.write("Inbox/Today.md", "# Today\n", { waitForContent: true });
|
|
538
|
+
await expect(await obsidian.metadata.frontmatter("Inbox/Today.md")).toBeNull();
|
|
539
|
+
await obsidian.plugin("my-plugin").reload({
|
|
540
|
+
waitUntilReady: true,
|
|
541
|
+
readyOptions: {
|
|
542
|
+
commandId: "my-plugin:refresh",
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
parseNoteDocument(await vault.read("Inbox/Today.md"));
|
|
418
546
|
```
|
|
419
547
|
|
|
420
548
|
## App And Commands
|
|
@@ -431,11 +559,24 @@ to the real `obsidian` CLI:
|
|
|
431
559
|
- `obsidian.command(id).run()`
|
|
432
560
|
- `obsidian.dev.dom({ ... })`
|
|
433
561
|
- `obsidian.dev.eval(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?)`
|
|
434
570
|
- `obsidian.dev.screenshot(path)`
|
|
435
571
|
- `obsidian.tabs()`
|
|
436
572
|
- `obsidian.workspace()`
|
|
437
573
|
- `obsidian.open({ file? | path?, newTab? })`
|
|
438
574
|
- `obsidian.openTab({ file?, group?, view? })`
|
|
575
|
+
- `obsidian.sleep(ms)`
|
|
576
|
+
- `obsidian.waitForActiveFile(path)`
|
|
577
|
+
- `obsidian.waitForConsoleMessage(predicate)`
|
|
578
|
+
- `obsidian.waitForNotice(predicate)`
|
|
579
|
+
- `obsidian.waitForRuntimeError(predicate)`
|
|
439
580
|
|
|
440
581
|
Example:
|
|
441
582
|
|
|
@@ -463,6 +604,45 @@ test("reloads the app and runs a plugin command when it becomes available", asyn
|
|
|
463
604
|
`obsidian.app.restart()` waits for the app to come back by default. Pass
|
|
464
605
|
`{ waitUntilReady: false }` if you need to manage readiness explicitly.
|
|
465
606
|
|
|
607
|
+
## Vault, Metadata, And Plugin Wait Helpers
|
|
608
|
+
|
|
609
|
+
The higher-level vault and plugin handles now expose the most common polling
|
|
610
|
+
patterns directly, so tests do not need to hand-roll `waitFor()` loops around
|
|
611
|
+
content reads, command discovery, or plugin data migration:
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
test("waits for generated content and plugin state", async ({ obsidian, sandbox, vault }) => {
|
|
615
|
+
const plugin = obsidian.plugin("quickadd");
|
|
616
|
+
|
|
617
|
+
await vault.write("queue.md", "pending", {
|
|
618
|
+
waitForContent: true,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await vault.waitForContent("queue.md", (content) => content.includes("pending"));
|
|
622
|
+
await sandbox.writeNote({
|
|
623
|
+
path: "Inbox/Today.md",
|
|
624
|
+
frontmatter: {
|
|
625
|
+
tags: ["daily"],
|
|
626
|
+
},
|
|
627
|
+
body: "# Today\n",
|
|
628
|
+
});
|
|
629
|
+
await sandbox.waitForFrontmatter("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;
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
await plugin.waitForData<{ migrations: Record<string, boolean> }>(
|
|
638
|
+
(data) => data.migrations.quickadd_v2 === true,
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
If you just need time to pass without inventing a fake polling condition, use
|
|
644
|
+
`await obsidian.sleep(ms)`.
|
|
645
|
+
|
|
466
646
|
Workspace and tab readers return parsed structures, so you can inspect layout
|
|
467
647
|
state without writing custom parsers in every test:
|
|
468
648
|
|
|
@@ -498,11 +678,44 @@ test("inspects live UI state", async ({ obsidian }) => {
|
|
|
498
678
|
});
|
|
499
679
|
```
|
|
500
680
|
|
|
501
|
-
`obsidian.dev.eval()`
|
|
502
|
-
|
|
681
|
+
`obsidian.dev.eval()` now uses a structured JSON envelope and throws remote
|
|
682
|
+
stack information when the evaluated code fails. Use `obsidian.dev.evalRaw()`
|
|
683
|
+
only when you intentionally need the unstructured CLI output. `dev.dom()` and
|
|
684
|
+
`dev.screenshot()` remain the safer wrappers around the built-in developer CLI
|
|
503
685
|
commands. Screenshot behavior depends on the active desktop environment, so
|
|
504
686
|
start by validating it locally before relying on it in automation.
|
|
505
687
|
|
|
688
|
+
## Layer Boundaries
|
|
689
|
+
|
|
690
|
+
- `vault` stays filesystem-only. If the behavior depends on Obsidian parsing or
|
|
691
|
+
workspace state, it does not belong there.
|
|
692
|
+
- `sandbox.readNote()` parses file content only. It does not imply that
|
|
693
|
+
Obsidian has indexed the note.
|
|
694
|
+
- `sandbox.frontmatter()` and `obsidian.metadata.*` read metadata-cache state,
|
|
695
|
+
which is the right layer for frontmatter synchronization and race-sensitive
|
|
696
|
+
tests.
|
|
697
|
+
- `obsidian.dev.eval()` is structured but still low-level. Prefer the
|
|
698
|
+
higher-level metadata, sandbox, wait, plugin, and matcher helpers first.
|
|
699
|
+
|
|
700
|
+
## Migration Notes
|
|
701
|
+
|
|
702
|
+
- If you relied on raw `obsidian.dev.eval()` output strings, switch those calls
|
|
703
|
+
to `obsidian.dev.evalRaw()`. The structured `eval()` path now expects JSON-safe
|
|
704
|
+
values and throws `DevEvalError` with the remote stack.
|
|
705
|
+
- Prefer `sandbox.writeNote()` over hand-built YAML strings when the test is
|
|
706
|
+
describing note content rather than string formatting.
|
|
707
|
+
- Prefer `plugin.updateDataAndReload()` or `plugin.withPatchedData()` over open-
|
|
708
|
+
coded patch/reload/restore sequences.
|
|
709
|
+
|
|
710
|
+
## Regression Matrix
|
|
711
|
+
|
|
712
|
+
- Metadata waits cover delayed file-cache population and frontmatter
|
|
713
|
+
synchronization after note writes.
|
|
714
|
+
- Failure artifacts capture active note content, parsed frontmatter, recent
|
|
715
|
+
console/notices/runtime errors, and workspace snapshots.
|
|
716
|
+
- Lifecycle cleanup restores tracked plugin data before disabling plugins and
|
|
717
|
+
removes sandbox content after teardown.
|
|
718
|
+
|
|
506
719
|
## End-To-End Workflow
|
|
507
720
|
|
|
508
721
|
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 { A as
|
|
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-C4cj443K.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-CYyOdRP1.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 CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginHandle, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, 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
|
|
2
|
-
|
|
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-BprSx6U1.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 };
|
package/dist/matchers.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { b as NoteMatcherOptions, v as NoteFrontmatter } from "./types-C4cj443K.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.
|
|
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,14 @@ expect.extend({
|
|
|
35
36
|
pass
|
|
36
37
|
};
|
|
37
38
|
},
|
|
39
|
+
async toHaveFrontmatter(target, targetPath, expected) {
|
|
40
|
+
const actual = await target.frontmatter(targetPath);
|
|
41
|
+
const pass = isDeepStrictEqual(actual, expected);
|
|
42
|
+
return {
|
|
43
|
+
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)}`,
|
|
44
|
+
pass
|
|
45
|
+
};
|
|
46
|
+
},
|
|
38
47
|
async toHaveJsonFile(target, targetPath) {
|
|
39
48
|
if (!await target.exists(targetPath)) return {
|
|
40
49
|
message: () => `Expected JSON file to exist: ${targetPath}`,
|
|
@@ -53,6 +62,25 @@ expect.extend({
|
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
},
|
|
65
|
+
async toHaveNote(target, targetPath, expected) {
|
|
66
|
+
let actual;
|
|
67
|
+
try {
|
|
68
|
+
actual = parseNoteDocument(await target.read(targetPath));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
message: () => `Expected vault path "${targetPath}" to be readable as a note, but reading failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
+
pass: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const matchesFrontmatter = expected.frontmatter === void 0 || isDeepStrictEqual(actual.frontmatter, expected.frontmatter);
|
|
76
|
+
const matchesBody = expected.body === void 0 || actual.body === expected.body;
|
|
77
|
+
const matchesBodyIncludes = expected.bodyIncludes === void 0 || actual.body.includes(expected.bodyIncludes);
|
|
78
|
+
const pass = matchesFrontmatter && matchesBody && matchesBodyIncludes;
|
|
79
|
+
return {
|
|
80
|
+
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)}`,
|
|
81
|
+
pass
|
|
82
|
+
};
|
|
83
|
+
},
|
|
56
84
|
async toHaveOpenTab(target, title, viewType) {
|
|
57
85
|
const pass = (await target.tabs()).some((tab) => tab.title === title && (viewType === void 0 || tab.viewType === viewType));
|
|
58
86
|
return {
|
|
@@ -69,7 +97,7 @@ expect.extend({
|
|
|
69
97
|
};
|
|
70
98
|
},
|
|
71
99
|
async toHaveEditorTextContaining(target, needle) {
|
|
72
|
-
const actual = await target.dev.
|
|
100
|
+
const actual = await target.dev.editorText();
|
|
73
101
|
const pass = typeof actual === "string" && actual.includes(needle);
|
|
74
102
|
return {
|
|
75
103
|
message: () => pass ? `Expected editor text not to contain "${needle}"` : `Expected editor text to contain "${needle}", received ${JSON.stringify(actual)}`,
|
package/dist/matchers.mjs.map
CHANGED
|
@@ -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(target: SandboxApi, targetPath: string, expected: NoteFrontmatter) {\n const actual = await target.frontmatter(targetPath);\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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"}
|