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 +432 -0
- package/dist/index.d.mts +22 -0
- package/dist/index.mjs +2 -0
- package/dist/matchers.d.mts +26 -0
- package/dist/matchers.mjs +94 -0
- package/dist/matchers.mjs.map +1 -0
- package/dist/sandbox-BhesE1S4.mjs +565 -0
- package/dist/sandbox-BhesE1S4.mjs.map +1 -0
- package/dist/types-5UxOZM7r.d.mts +152 -0
- package/dist/vitest.d.mts +46 -0
- package/dist/vitest.mjs +182 -0
- package/dist/vitest.mjs.map +1 -0
- package/package.json +45 -0
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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,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"}
|