obsidian-e2e 0.0.0-next.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 ADDED
@@ -0,0 +1,432 @@
1
+ # obsidian-e2e
2
+
3
+ Vitest-first end-to-end test utilities for Obsidian plugins.
4
+
5
+ `obsidian-e2e` is a thin testing library around a live Obsidian vault and the
6
+ globally installed `obsidian` CLI. It stays plugin-agnostic on purpose: you get
7
+ generic fixtures for Obsidian, vault access, and per-test sandboxes, then opt
8
+ into plugin-specific behavior through `obsidian.plugin(id)`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add -D obsidian-e2e
14
+ ```
15
+
16
+ Requirements:
17
+
18
+ - Obsidian must be installed locally.
19
+ - The `obsidian` CLI must already be available on `PATH`.
20
+ - Your target vault must be open and reachable from the CLI.
21
+
22
+ ## Public Entry Points
23
+
24
+ - `obsidian-e2e`
25
+ - low-level client and shared types
26
+ - `obsidian-e2e/vitest`
27
+ - `createObsidianTest()`
28
+ - `createPluginTest()`
29
+ - `obsidian-e2e/matchers`
30
+ - optional `expect` matchers for vault and sandbox assertions
31
+
32
+ ## Setup
33
+
34
+ `tests/setup.ts`
35
+
36
+ ```ts
37
+ import { createObsidianTest } from "obsidian-e2e/vitest";
38
+ import "obsidian-e2e/matchers";
39
+
40
+ export const test = createObsidianTest({
41
+ vault: "dev",
42
+ bin: process.env.OBSIDIAN_BIN ?? "obsidian",
43
+ sandboxRoot: "__obsidian_e2e__",
44
+ timeoutMs: 5_000,
45
+ });
46
+ ```
47
+
48
+ `vite.config.ts`
49
+
50
+ ```ts
51
+ import { defineConfig } from "vite-plus";
52
+
53
+ export default defineConfig({
54
+ test: {
55
+ fileParallelism: false,
56
+ maxWorkers: 1,
57
+ },
58
+ });
59
+ ```
60
+
61
+ Run Obsidian-backed tests serially. A live Obsidian app and shared vault are
62
+ not safe to hit from multiple Vitest workers at once, so `fileParallelism: false`
63
+ and `maxWorkers: 1` should be treated as the default, not as an optimization.
64
+
65
+ ## Writing Tests
66
+
67
+ ```ts
68
+ import { expect } from "vite-plus/test";
69
+ import { test } from "./setup";
70
+
71
+ test("reloads a plugin after patching its data file", async ({ obsidian, vault, sandbox }) => {
72
+ const plugin = obsidian.plugin("my-plugin");
73
+
74
+ await sandbox.write("tpl.md", "template body");
75
+ await vault.write("notes/source.md", "existing");
76
+
77
+ await plugin.data<{ enabled: boolean }>().patch((draft) => {
78
+ draft.enabled = true;
79
+ });
80
+
81
+ await plugin.reload();
82
+
83
+ await expect(sandbox).toHaveFile("tpl.md");
84
+ await expect(vault).toHaveFileContaining("notes/source.md", "existing");
85
+ });
86
+ ```
87
+
88
+ Fixture summary:
89
+
90
+ - `obsidian`
91
+ - low-level access to `app`, `command(id)`, `commands()`, `exec`, `execText`,
92
+ `execJson`, `waitFor`, `vaultPath`, and `plugin(id)`
93
+ - `vault`
94
+ - reads and writes anywhere in the vault rooted at the active Obsidian vault
95
+ - `sandbox`
96
+ - a per-test disposable directory under `sandboxRoot`; automatically cleaned
97
+ up after each test
98
+
99
+ Plugin data mutations are snapshotted on first write and restored automatically
100
+ after each test. Sandbox files are also cleaned up automatically.
101
+
102
+ ## Plugin Test Helper
103
+
104
+ If you are testing one plugin repeatedly, `createPluginTest()` gives you a
105
+ first-class `plugin` fixture and optional seed helpers for vault files and
106
+ plugin data:
107
+
108
+ ```ts
109
+ import { createPluginTest } from "obsidian-e2e/vitest";
110
+
111
+ export const test = createPluginTest({
112
+ vault: "dev",
113
+ pluginId: "quickadd",
114
+ pluginFilter: "community",
115
+ seedPluginData: { enabled: true },
116
+ seedVault: {
117
+ "fixtures/template.md": "template body",
118
+ "fixtures/state.json": { json: { ready: true } },
119
+ },
120
+ });
121
+ ```
122
+
123
+ `createPluginTest()`:
124
+
125
+ - injects `plugin` alongside `obsidian`, `vault`, and `sandbox`
126
+ - enables the target plugin for the test when needed and restores the prior
127
+ enabled/disabled state afterward
128
+ - seeds vault files before each test and restores the original files afterward
129
+ - seeds `data.json` through the normal plugin snapshot/restore path
130
+ - supports the same opt-in failure artifact capture as `createObsidianTest()`
131
+
132
+ Example:
133
+
134
+ ```ts
135
+ import { expect } from "vite-plus/test";
136
+ import { test } from "./setup";
137
+
138
+ test("runs against a seeded plugin fixture", async ({ plugin, vault }) => {
139
+ await expect(plugin.data<{ enabled: boolean }>().read()).resolves.toEqual({
140
+ enabled: true,
141
+ });
142
+ await expect(vault.read("fixtures/template.md")).resolves.toBe("template body");
143
+
144
+ await plugin.reload();
145
+ });
146
+ ```
147
+
148
+ ## Failure Artifacts
149
+
150
+ Both fixture families support opt-in artifact capture:
151
+
152
+ - `createObsidianTest({ artifactsDir, captureOnFailure })`
153
+ - `createPluginTest({ artifactsDir, captureOnFailure, ... })`
154
+
155
+ Example:
156
+
157
+ ```ts
158
+ import { createObsidianTest, createPluginTest } from "obsidian-e2e/vitest";
159
+
160
+ export const test = createObsidianTest({
161
+ vault: "dev",
162
+ captureOnFailure: true,
163
+ });
164
+
165
+ export const pluginTest = createPluginTest({
166
+ vault: "dev",
167
+ pluginId: "quickadd",
168
+ artifactsDir: ".artifacts",
169
+ captureOnFailure: {
170
+ screenshot: false,
171
+ },
172
+ });
173
+ ```
174
+
175
+ When `captureOnFailure` is enabled, failed tests write artifacts under
176
+ `.obsidian-e2e-artifacts` by default, or under `artifactsDir` if you set one.
177
+ Each failed test gets its own directory named from the test name plus a stable
178
+ task-id suffix, for example:
179
+
180
+ ```txt
181
+ .obsidian-e2e-artifacts/
182
+ writes-useful-artifacts-abcdef12/
183
+ ```
184
+
185
+ `createObsidianTest()` captures:
186
+
187
+ - `active-file.json`
188
+ - `dom.txt`
189
+ - `editor.json`
190
+ - `tabs.json`
191
+ - `workspace.json`
192
+ - `screenshot.png` when screenshot capture succeeds
193
+
194
+ `createPluginTest()` adds:
195
+
196
+ - `<pluginId>-data.json`
197
+
198
+ Artifact collection is best-effort. If a specific capture fails, the test still
199
+ fails for its original reason and the framework writes a neighboring
200
+ `*.error.txt` file instead. Screenshot capture is the most environment-sensitive
201
+ part of the set: desktop permissions, display availability, or Obsidian state
202
+ can prevent `screenshot.png` from being produced, in which case you should
203
+ expect `screenshot.error.txt` instead.
204
+
205
+ ## Maintainer CI And Releases
206
+
207
+ This repo now ships with a hardened CI and release flow built around Vite+
208
+ workflow setup, Changesets release orchestration, and npm trusted publishing
209
+ through GitHub OIDC.
210
+
211
+ At a high level:
212
+
213
+ - CI installs the toolchain with `setup-vp`, then runs `vp check`,
214
+ `vp test`, and `vp pack`.
215
+ - When CI fails after artifact capture is enabled in tests, it uploads
216
+ `.obsidian-e2e-artifacts` so maintainers can inspect the same failure
217
+ snapshots produced locally.
218
+ - Releases go through Changesets PRs. Merge the version PR that
219
+ Changesets opens, then let the release workflow publish to npm.
220
+
221
+ Maintainer setup notes:
222
+
223
+ - Configure npm trusted publishing for this package and repository so the
224
+ GitHub release workflow can publish without a long-lived npm token.
225
+ - Grant the publish job `id-token: write` so GitHub can mint the OIDC token npm
226
+ expects, and keep the release workflow permissions aligned with the write
227
+ actions it needs, such as `contents: write` and `pull-requests: write` for
228
+ Changesets automation.
229
+ - If you protect publishing behind a GitHub environment, attach that
230
+ environment to the release job and allow the workflow to use it.
231
+
232
+ ## Matchers
233
+
234
+ Import `obsidian-e2e/matchers` once in your test setup to register:
235
+
236
+ - `toHaveActiveFile(path)`
237
+ - `toHaveCommand(commandId)`
238
+ - `toHaveEditorTextContaining(needle)`
239
+ - `toHaveFile(path)`
240
+ - `toHaveFileContaining(path, needle)`
241
+ - `toHaveJsonFile(path)`
242
+ - `toHaveOpenTab(title, viewType?)`
243
+ - `toHavePluginData(expected)`
244
+ - `toHaveWorkspaceNode(label)`
245
+
246
+ Example:
247
+
248
+ ```ts
249
+ import { expect } from "vite-plus/test";
250
+ import { test } from "./setup";
251
+
252
+ test("writes valid JSON into the sandbox", async ({ sandbox }) => {
253
+ await sandbox.json("config.json").write({ enabled: true });
254
+
255
+ await expect(sandbox).toHaveJsonFile("config.json");
256
+ });
257
+
258
+ test("asserts active Obsidian state", async ({ obsidian, plugin }) => {
259
+ await expect(obsidian).toHaveCommand("quickadd:run-choice");
260
+ await expect(obsidian).toHaveActiveFile("Inbox/Today.md");
261
+ await expect(obsidian).toHaveEditorTextContaining("Today");
262
+ await expect(obsidian).toHaveOpenTab("Today", "markdown");
263
+ await expect(obsidian).toHaveWorkspaceNode("main");
264
+ await expect(plugin).toHavePluginData({ enabled: true });
265
+ });
266
+ ```
267
+
268
+ ## Low-Level Client
269
+
270
+ If you need to work below the fixture layer:
271
+
272
+ ```ts
273
+ import { createObsidianClient } from "obsidian-e2e";
274
+
275
+ const obsidian = createObsidianClient({
276
+ vault: "dev",
277
+ bin: "obsidian",
278
+ });
279
+
280
+ await obsidian.verify();
281
+ await obsidian.exec("plugin:reload", { id: "my-plugin" });
282
+ ```
283
+
284
+ ## App And Commands
285
+
286
+ The client now exposes app-level helpers and command helpers that map directly
287
+ to the real `obsidian` CLI:
288
+
289
+ - `obsidian.app.version()`
290
+ - `obsidian.app.reload()`
291
+ - `obsidian.app.restart()`
292
+ - `obsidian.app.waitUntilReady()`
293
+ - `obsidian.commands({ filter? })`
294
+ - `obsidian.command(id).exists()`
295
+ - `obsidian.command(id).run()`
296
+ - `obsidian.dev.dom({ ... })`
297
+ - `obsidian.dev.eval(code)`
298
+ - `obsidian.dev.screenshot(path)`
299
+ - `obsidian.tabs()`
300
+ - `obsidian.workspace()`
301
+ - `obsidian.open({ file? | path?, newTab? })`
302
+ - `obsidian.openTab({ file?, group?, view? })`
303
+
304
+ Example:
305
+
306
+ ```ts
307
+ import { expect } from "vite-plus/test";
308
+ import { test } from "./setup";
309
+
310
+ test("reloads the app and runs a plugin command when it becomes available", async ({
311
+ obsidian,
312
+ }) => {
313
+ await obsidian.app.waitUntilReady();
314
+
315
+ const commandId = "quickadd:run-choice";
316
+
317
+ if (await obsidian.command(commandId).exists()) {
318
+ await obsidian.command(commandId).run();
319
+ }
320
+
321
+ await obsidian.app.reload();
322
+
323
+ await expect(obsidian.commands({ filter: "quickadd:" })).resolves.toContain(commandId);
324
+ });
325
+ ```
326
+
327
+ `obsidian.app.restart()` waits for the app to come back by default. Pass
328
+ `{ waitUntilReady: false }` if you need to manage readiness explicitly.
329
+
330
+ Workspace and tab readers return parsed structures, so you can inspect layout
331
+ state without writing custom parsers in every test:
332
+
333
+ ```ts
334
+ test("opens a note into a new tab and finds it in the workspace", async ({ obsidian }) => {
335
+ await obsidian.open({
336
+ newTab: true,
337
+ path: "Inbox/Today.md",
338
+ });
339
+
340
+ const tabs = await obsidian.tabs();
341
+ const workspace = await obsidian.workspace();
342
+
343
+ expect(tabs.some((tab) => tab.title === "Today")).toBe(true);
344
+ expect(workspace.some((node) => node.label === "main")).toBe(true);
345
+ });
346
+ ```
347
+
348
+ For deeper UI inspection, the `dev` namespace exposes the desktop developer
349
+ commands:
350
+
351
+ ```ts
352
+ test("inspects live UI state", async ({ obsidian }) => {
353
+ const titles = await obsidian.dev.dom({
354
+ all: true,
355
+ selector: ".workspace-tab-header-inner-title",
356
+ text: true,
357
+ });
358
+
359
+ expect(titles).toContain("Today");
360
+
361
+ await obsidian.dev.screenshot("artifacts/today.png");
362
+ });
363
+ ```
364
+
365
+ `obsidian.dev.eval()` is the low-level escape hatch, while `dev.dom()` and
366
+ `dev.screenshot()` give you safer wrappers around the built-in developer CLI
367
+ commands. Screenshot behavior depends on the active desktop environment, so
368
+ start by validating it locally before relying on it in automation.
369
+
370
+ ## End-To-End Workflow
371
+
372
+ Putting it together, a realistic plugin test usually looks like this:
373
+
374
+ ```ts
375
+ import { expect } from "vite-plus/test";
376
+ import { createPluginTest } from "obsidian-e2e/vitest";
377
+ import "obsidian-e2e/matchers";
378
+
379
+ const test = createPluginTest({
380
+ vault: "dev",
381
+ pluginId: "quickadd",
382
+ pluginFilter: "community",
383
+ seedPluginData: {
384
+ macros: [],
385
+ },
386
+ seedVault: {
387
+ "fixtures/template.md": "Hello from template",
388
+ "Inbox/Today.md": "# Today\n",
389
+ },
390
+ });
391
+
392
+ test("runs a seeded workflow end to end", async ({ obsidian, plugin, vault }) => {
393
+ await expect(obsidian).toHaveCommand("quickadd:run-choice");
394
+ await expect(plugin).toHavePluginData({
395
+ macros: [],
396
+ });
397
+
398
+ if (await obsidian.command("quickadd:run-choice").exists()) {
399
+ await obsidian.command("quickadd:run-choice").run();
400
+ }
401
+
402
+ await obsidian.open({
403
+ path: "Inbox/Today.md",
404
+ });
405
+
406
+ await expect(obsidian).toHaveActiveFile("Inbox/Today.md");
407
+ await expect(vault).toHaveFile("fixtures/template.md");
408
+
409
+ const headers = await obsidian.dev.dom({
410
+ all: true,
411
+ selector: ".workspace-tab-header-inner-title",
412
+ text: true,
413
+ });
414
+
415
+ expect(headers).toContain("Today");
416
+ });
417
+ ```
418
+
419
+ That pattern keeps tests readable:
420
+
421
+ - use `createPluginTest()` when one plugin is the main subject under test
422
+ - seed only the files and plugin data needed for that case
423
+ - prefer Obsidian-aware matchers over ad hoc CLI parsing
424
+ - drop to `obsidian.dev.*` only when filesystem and command assertions are not enough
425
+
426
+ ## Notes
427
+
428
+ - This package is a testing library, not a custom runner.
429
+ - It is designed for real Obsidian-backed integration and e2e flows, not for
430
+ mocked unit tests.
431
+ - Headless CI for desktop Obsidian is environment-specific; start by getting
432
+ tests reliable locally before automating them.
@@ -0,0 +1,22 @@
1
+ import { C as WaitForOptions, E as WorkspaceTab, S as VaultApi, T as WorkspaceOptions, _ as PluginHandle, a as DevDomResult, b as SandboxApi, c as JsonFile, d as ObsidianArg, f as ObsidianClient, g as OpenTabOptions, h as OpenFileOptions, i as DevDomQueryOptions, l as JsonFileUpdater, m as ObsidianDevHandle, n as CommandTransport, o as ExecOptions, p as ObsidianCommandHandle, r as CreateObsidianClientOptions, s as ExecResult, t as CommandListOptions, u as ObsidianAppHandle, w as WorkspaceNode, x as TabsOptions, y as RestartAppOptions } from "./types-5UxOZM7r.mjs";
2
+
3
+ //#region src/core/client.d.ts
4
+ declare function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient;
5
+ //#endregion
6
+ //#region src/vault/sandbox.d.ts
7
+ interface CreateSandboxApiOptions {
8
+ obsidian: ObsidianClient;
9
+ sandboxRoot: string;
10
+ testName: string;
11
+ }
12
+ declare function createSandboxApi(options: CreateSandboxApiOptions): Promise<SandboxApi>;
13
+ //#endregion
14
+ //#region src/vault/vault.d.ts
15
+ interface CreateVaultApiOptions {
16
+ obsidian: ObsidianClient;
17
+ root?: string;
18
+ }
19
+ declare function createVaultApi(options: CreateVaultApiOptions): VaultApi;
20
+ //#endregion
21
+ export { 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 WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, createObsidianClient, createSandboxApi, createVaultApi };
22
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as createVaultApi, r as createObsidianClient, t as createSandboxApi } from "./sandbox-BhesE1S4.mjs";
2
+ export { createObsidianClient, createSandboxApi, createVaultApi };
@@ -0,0 +1,26 @@
1
+ //#region src/matchers.d.ts
2
+ declare module "vite-plus/test" {
3
+ interface Assertion<T = any> {
4
+ toHaveActiveFile(path: string): Promise<T>;
5
+ toHaveCommand(commandId: string): Promise<T>;
6
+ toHaveEditorTextContaining(needle: string): Promise<T>;
7
+ toHaveFile(path: string): Promise<T>;
8
+ toHaveFileContaining(path: string, needle: string): Promise<T>;
9
+ toHaveJsonFile(path: string): Promise<T>;
10
+ toHaveOpenTab(title: string, viewType?: string): Promise<T>;
11
+ toHavePluginData(expected: unknown): Promise<T>;
12
+ toHaveWorkspaceNode(label: string): Promise<T>;
13
+ }
14
+ interface AsymmetricMatchersContaining {
15
+ toHaveActiveFile(path: string): void;
16
+ toHaveCommand(commandId: string): void;
17
+ toHaveEditorTextContaining(needle: string): void;
18
+ toHaveFile(path: string): void;
19
+ toHaveFileContaining(path: string, needle: string): void;
20
+ toHaveJsonFile(path: string): void;
21
+ toHaveOpenTab(title: string, viewType?: string): void;
22
+ toHavePluginData(expected: unknown): void;
23
+ toHaveWorkspaceNode(label: string): void;
24
+ }
25
+ }
26
+ //# sourceMappingURL=matchers.d.mts.map
@@ -0,0 +1,94 @@
1
+ import { expect } from "vite-plus/test";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ //#region src/matchers.ts
4
+ expect.extend({
5
+ async toHaveActiveFile(target, targetPath) {
6
+ const actual = await target.dev.eval("app.workspace.getActiveFile()?.path ?? null");
7
+ const pass = actual === targetPath;
8
+ return {
9
+ message: () => pass ? `Expected active file not to be "${targetPath}"` : `Expected active file to be "${targetPath}", received ${JSON.stringify(actual)}`,
10
+ pass
11
+ };
12
+ },
13
+ async toHaveCommand(target, commandId) {
14
+ const pass = await target.command(commandId).exists();
15
+ return {
16
+ message: () => pass ? `Expected Obsidian command not to exist: ${commandId}` : `Expected Obsidian command to exist: ${commandId}`,
17
+ pass
18
+ };
19
+ },
20
+ async toHaveFile(target, targetPath) {
21
+ const pass = await target.exists(targetPath);
22
+ return {
23
+ message: () => pass ? `Expected vault path not to exist: ${targetPath}` : `Expected vault path to exist: ${targetPath}`,
24
+ pass
25
+ };
26
+ },
27
+ async toHaveFileContaining(target, targetPath, needle) {
28
+ if (!await target.exists(targetPath)) return {
29
+ message: () => `Expected vault path to exist: ${targetPath}`,
30
+ pass: false
31
+ };
32
+ const pass = (await target.read(targetPath)).includes(needle);
33
+ return {
34
+ message: () => pass ? `Expected vault path "${targetPath}" not to contain "${needle}"` : `Expected vault path "${targetPath}" to contain "${needle}"`,
35
+ pass
36
+ };
37
+ },
38
+ async toHaveJsonFile(target, targetPath) {
39
+ if (!await target.exists(targetPath)) return {
40
+ message: () => `Expected JSON file to exist: ${targetPath}`,
41
+ pass: false
42
+ };
43
+ try {
44
+ await target.json(targetPath).read();
45
+ return {
46
+ message: () => `Expected JSON file "${targetPath}" not to be valid JSON`,
47
+ pass: true
48
+ };
49
+ } catch (error) {
50
+ return {
51
+ message: () => `Expected JSON file "${targetPath}" to be valid JSON, but parsing failed: ${error instanceof Error ? error.message : String(error)}`,
52
+ pass: false
53
+ };
54
+ }
55
+ },
56
+ async toHaveOpenTab(target, title, viewType) {
57
+ const pass = (await target.tabs()).some((tab) => tab.title === title && (viewType === void 0 || tab.viewType === viewType));
58
+ return {
59
+ message: () => pass ? `Expected no open tab matching "${title}"${viewType ? ` with view type "${viewType}"` : ""}` : `Expected an open tab matching "${title}"${viewType ? ` with view type "${viewType}"` : ""}`,
60
+ pass
61
+ };
62
+ },
63
+ async toHavePluginData(target, expected) {
64
+ const actual = await target.data().read();
65
+ const pass = isDeepStrictEqual(actual, expected);
66
+ return {
67
+ message: () => pass ? `Expected plugin data not to equal ${JSON.stringify(expected)}` : `Expected plugin data to equal ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`,
68
+ pass
69
+ };
70
+ },
71
+ async toHaveEditorTextContaining(target, needle) {
72
+ const actual = await target.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null");
73
+ const pass = typeof actual === "string" && actual.includes(needle);
74
+ return {
75
+ message: () => pass ? `Expected editor text not to contain "${needle}"` : `Expected editor text to contain "${needle}", received ${JSON.stringify(actual)}`,
76
+ pass
77
+ };
78
+ },
79
+ async toHaveWorkspaceNode(target, label) {
80
+ const pass = hasWorkspaceNode(await target.workspace(), label);
81
+ return {
82
+ message: () => pass ? `Expected workspace not to contain node "${label}"` : `Expected workspace to contain node "${label}"`,
83
+ pass
84
+ };
85
+ }
86
+ });
87
+ function hasWorkspaceNode(nodes, label) {
88
+ for (const node of nodes) if (node.label === label || hasWorkspaceNode(node.children, label)) return true;
89
+ return false;
90
+ }
91
+ //#endregion
92
+ export {};
93
+
94
+ //# sourceMappingURL=matchers.mjs.map
@@ -0,0 +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"}