{"id":47665953,"url":"https://github.com/mnaoumov/obsidian-integration-testing","last_synced_at":"2026-05-04T07:04:03.103Z","repository":{"id":346516535,"uuid":"1189216814","full_name":"mnaoumov/obsidian-integration-testing","owner":"mnaoumov","description":"Simplifies integration testing of Obsidian plugins.","archived":false,"fork":false,"pushed_at":"2026-05-04T03:23:37.000Z","size":6825,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-04T04:17:37.770Z","etag":null,"topics":["integration-testing","obsidian","obsidian-md"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mnaoumov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"buy_me_a_coffee":"mnaoumov"}},"created_at":"2026-03-23T05:04:46.000Z","updated_at":"2026-05-04T03:23:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mnaoumov/obsidian-integration-testing","commit_stats":null,"previous_names":["mnaoumov/obsidian-integration-testing"],"tags_count":38,"template":false,"template_full_name":null,"purl":"pkg:github/mnaoumov/obsidian-integration-testing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnaoumov%2Fobsidian-integration-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnaoumov%2Fobsidian-integration-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnaoumov%2Fobsidian-integration-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnaoumov%2Fobsidian-integration-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mnaoumov","download_url":"https://codeload.github.com/mnaoumov/obsidian-integration-testing/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnaoumov%2Fobsidian-integration-testing/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32597943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["integration-testing","obsidian","obsidian-md"],"created_at":"2026-04-02T11:57:20.331Z","updated_at":"2026-05-04T07:04:03.096Z","avatar_url":"https://github.com/mnaoumov.png","language":"TypeScript","funding_links":["https://buymeacoffee.com/mnaoumov","https://www.buymeacoffee.com/mnaoumov"],"categories":[],"sub_categories":[],"readme":"# obsidian-integration-testing\n\n[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?logo=buy-me-a-coffee\u0026logoColor=black)](https://www.buymeacoffee.com/mnaoumov)\n[![npm version](https://img.shields.io/npm/v/obsidian-integration-testing)](https://www.npmjs.com/package/obsidian-integration-testing)\n[![npm downloads](https://img.shields.io/npm/dm/obsidian-integration-testing)](https://www.npmjs.com/package/obsidian-integration-testing)\n[![GitHub release](https://img.shields.io/github/v/release/mnaoumov/obsidian-integration-testing)](https://github.com/mnaoumov/obsidian-integration-testing/releases)\n[![Coverage: 100%](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/mnaoumov/obsidian-integration-testing)\n\nA set of helpers that simplify integration testing of [Obsidian](https://obsidian.md/) plugins against a running Obsidian instance.\n\n## Installation\n\n```bash\nnpm install --save-dev obsidian-integration-testing\n```\n\n## Quick start\n\nThe global setup expects your built plugin in `dist/dev` or `dist/build` (whichever has a newer `main.js`), with a `manifest.json` at the root of the chosen folder. The setup creates a temporary vault, copies the build into it, and enables the plugin.\n\n### Vitest\n\n```ts\n// vitest.config.ts\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n  },\n});\n```\n\nTo get Vitest module augmentations (`environmentOptions.obsidianTransport`, `inject('obsidianTransport')`, `inject('tempVaultPath')`), add a side-effect import in your test setup or config:\n\n```ts\nimport 'obsidian-integration-testing/vitest/typings';\n```\n\nOr add it to `compilerOptions.types` in your `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"types\": [\"obsidian-integration-testing/vitest/typings\"]\n  }\n}\n```\n\n### Jest\n\n```ts\n// jest.config.ts\nexport default {\n  globalSetup: 'obsidian-integration-testing/jest-global-setup',\n};\n```\n\nTo configure transport options with Jest, populate `globalThis.__obsidianIntegrationTesting` before the global setup runs (e.g., in a setup file or via Jest `globals`):\n\n```ts\nglobalThis.__obsidianIntegrationTesting = {\n  transportOptions: { type: 'obsidian-cdp' },\n};\n```\n\nAfter setup, `globalThis.__obsidianIntegrationTesting.tempVaultPath` is available in test workers.\n\nBy default this uses the **`CLI` transport** (requires `CLI` enabled in Obsidian settings). See [Transport modes](#transport-modes) for alternatives.\n\n### Write integration tests\n\nUse `evalInObsidian()` to run code inside the Obsidian process. The `vaultPath` is optional — it defaults to `process.cwd()`:\n\n```ts\nimport { evalInObsidian } from 'obsidian-integration-testing';\n\n// Simple expression\nconst sum = await evalInObsidian({\n  args: { a: 2, b: 3 },\n  fn: ({ a, b }) =\u003e a + b\n});\n// sum === 5\n```\n\n### Access the Obsidian API\n\nEvery callback receives `app` (the Obsidian `App` instance) and `obsidianModule` (the full `obsidian` module):\n\n```ts\n// Read the vault config directory\nconst configDir = await evalInObsidian({\n  fn: ({ app }) =\u003e app.vault.configDir\n});\n\n// Use the obsidian module\nconst yaml = await evalInObsidian({\n  fn: ({ obsidianModule }) =\u003e obsidianModule.stringifyYaml({ key: 'value' })\n});\n\n// Access internal APIs\nconst title = await evalInObsidian({\n  fn: ({ app }) =\u003e app.title\n});\n```\n\n### Pass complex arguments\n\nArguments are JSON-serialized. You can even pass functions — they are serialized via `toString()`:\n\n```ts\nconst result = await evalInObsidian({\n  args: {\n    transform(x: number): number {\n      return x * 2;\n    },\n    value: 5\n  },\n  fn: ({ transform, value }) =\u003e transform(value)\n});\n// result === 10\n```\n\n### Persist non-serializable values across calls\n\nObsidian objects like `TFile` or `Editor` live in the Obsidian process and can't be returned to the test. Use `ContextId` to create a typed store that persists across calls:\n\n```ts\nimport type { TFile } from 'obsidian';\nimport { afterEach, beforeEach, describe, expect, it } from 'vitest';\nimport { ContextId, evalInObsidian } from 'obsidian-integration-testing';\n\ninterface Context {\n  file: TFile;\n}\n\nconst contextId = new ContextId\u003cContext\u003e();\n\nbeforeEach(async () =\u003e {\n  await evalInObsidian({\n    contextId,\n    fn: async ({ app, context }) =\u003e {\n      context.file = await app.vault.create('test.md', '# Hello');\n    }\n  });\n});\n\nafterEach(async () =\u003e {\n  await evalInObsidian({\n    contextId,\n    fn: async ({ app, context: { file } }) =\u003e {\n      await app.vault.delete(file);\n    }\n  });\n  await contextId.dispose();\n});\n\nit('should read the file path', async () =\u003e {\n  const path = await evalInObsidian({\n    contextId,\n    fn: ({ context: { file } }) =\u003e file.path\n  });\n  expect(path).toBe('test.md');\n});\n```\n\n### Create a temporary vault\n\nUse `TempVault` to create a disposable vault pre-populated with files:\n\n```ts\nimport type { TFile } from 'obsidian';\nimport { afterAll, beforeAll, describe, expect, it } from 'vitest';\nimport { ContextId, evalInObsidian, TempVault } from 'obsidian-integration-testing';\n\ninterface Context {\n  file: TFile;\n}\n\nconst vault = new TempVault();\n\nvault.populate({\n  'note.md': '# Hello',\n  'folder/nested.md': 'nested content',\n});\n\nconst contextId = new ContextId\u003cContext\u003e();\n\nbeforeAll(async () =\u003e {\n  await vault.register();\n\n  // Resolve the pre-populated file into a TFile and store it in the context\n  await evalInObsidian({\n    contextId,\n    fn: async ({ app, context }) =\u003e {\n      const file = app.vault.getFileByPath('note.md');\n      if (!file) {\n        throw new Error('File not found');\n      }\n      context.file = file;\n    },\n    vaultPath: vault.path\n  });\n});\n\nafterAll(async () =\u003e {\n  await contextId.dispose(vault.path);\n  await vault.dispose();\n});\n\nit('should read a pre-populated file', async () =\u003e {\n  const content = await evalInObsidian({\n    fn: ({ app }) =\u003e app.vault.adapter.read('note.md'),\n    vaultPath: vault.path\n  });\n  expect(content).toBe('# Hello');\n});\n\nit('should access the TFile from context', async () =\u003e {\n  const path = await evalInObsidian({\n    contextId,\n    fn: ({ context: { file } }) =\u003e file.path,\n    vaultPath: vault.path\n  });\n  expect(path).toBe('note.md');\n});\n```\n\nBoth `TempVault` and `ContextId` implement `AsyncDisposable`, so you can use `await using` for automatic cleanup.\n\nParent directories are created automatically. To create an empty folder, use a path ending with `/` and an empty string as content.\n\n### Test your plugin\n\nUse `getTempVault()` to get the temporary vault created by the global setup:\n\n**Vitest:**\n\n```ts\nimport { describe, expect, it } from 'vitest';\nimport { evalInObsidian } from 'obsidian-integration-testing';\nimport { getTempVault } from 'obsidian-integration-testing/vitest-global-setup';\n\ndescribe('my-plugin', () =\u003e {\n  const vault = getTempVault();\n\n  it('should be enabled', async () =\u003e {\n    const isEnabled = await evalInObsidian({\n      args: { pluginId: 'my-plugin' },\n      fn: ({ app, pluginId }) =\u003e app.plugins.enabledPlugins.has(pluginId),\n      vaultPath: vault.path\n    });\n    expect(isEnabled).toBe(true);\n  });\n\n  it('should create a file', async () =\u003e {\n    await evalInObsidian({\n      fn: async ({ app }) =\u003e {\n        await app.vault.create('test.md', '# Hello');\n      },\n      vaultPath: vault.path\n    });\n\n    const content = await evalInObsidian({\n      fn: ({ app }) =\u003e app.vault.adapter.read('test.md'),\n      vaultPath: vault.path\n    });\n    expect(content).toBe('# Hello');\n  });\n});\n```\n\n**Jest:**\n\n```ts\nimport { evalInObsidian } from 'obsidian-integration-testing';\nimport { getTempVault } from 'obsidian-integration-testing/jest-global-setup';\n\ndescribe('my-plugin', () =\u003e {\n  const vault = getTempVault();\n\n  it('should be enabled', async () =\u003e {\n    const isEnabled = await evalInObsidian({\n      args: { pluginId: 'my-plugin' },\n      fn: ({ app, pluginId }) =\u003e app.plugins.enabledPlugins.has(pluginId),\n      vaultPath: vault.path\n    });\n    expect(isEnabled).toBe(true);\n  });\n});\n```\n\n\u003e [!WARNING]\n\u003e\n\u003e **Parallelism:**\n\u003e\n\u003e The Obsidian `CLI` does not support executing multiple commands concurrently. If your test runner launches tests in parallel, `CLI` calls may collide and produce flaky failures. Disable file-level parallelism in your Vitest config:\n\u003e\n\u003e ```ts\n\u003e // vitest.config.ts\n\u003e export default defineConfig({\n\u003e   test: {\n\u003e     fileParallelism: false\n\u003e   }\n\u003e });\n\u003e ```\n\n\u0026nbsp;\n\n\u003e [!WARNING]\n\u003e\n\u003e **`evalInObsidian` limitations:**\n\u003e\n\u003e - The function is serialized via `toString()` and executed in a separate process. It must be **self-contained** — closures over local variables will not work.\n\u003e - Pass any needed values via `args`. Arguments must be **JSON-serializable** (strings, numbers, booleans, arrays, plain objects). Functions in `args` are supported — they are serialized via `toString()` with the same self-contained constraint.\n\u003e - The **return value** must also be JSON-serializable. You cannot return functions, class instances, `Map`, `Set`, DOM elements, or other non-serializable values.\n\u003e - Imports (`import`/`require`) are not available inside the function. Use `obsidianModule` to access the `obsidian` API, and `app` to access the Obsidian `App` instance.\n\n### Accessing internal APIs\n\nSince `evalInObsidian` runs inside a real Obsidian process, you have access to internal (undocumented) APIs like `app.plugins`, `app.commands`, `app.title`, etc. However, these are not declared in `obsidian.d.ts`, so TypeScript won't compile references to them. Here are the options to make it work, from best to worst:\n\n**1. Use `obsidian-typings`** (recommended) — install [`obsidian-typings`](https://www.npmjs.com/package/obsidian-typings) which declares the full internal API. Everything compiles with no extra work:\n\n```ts\n// With obsidian-typings installed — no casts needed\nconst title = await evalInObsidian({\n  fn: ({ app }) =\u003e app.title\n});\n```\n\n**2. Manual module augmentation** — declare only what you need:\n\n```ts\ndeclare module 'obsidian' {\n  interface App {\n    title: string;\n  }\n}\n\nconst title = await evalInObsidian({\n  fn: ({ app }) =\u003e app.title\n});\n```\n\n**3. `as any` / `@ts-expect-error` / `@ts-ignore`** (not recommended) — suppresses all type checking and hides real errors:\n\n```ts\nconst title = await evalInObsidian({\n  // @ts-expect-error -- accessing internal API\n  fn: ({ app }) =\u003e app.title\n});\n\n// or\nconst title2 = await evalInObsidian({\n  fn: ({ app }) =\u003e (app as any).title\n});\n```\n\n### Transport modes\n\nThe transport determines how the library communicates with Obsidian. Configure it via transport options in your test framework's config (see [Quick start](#quick-start)):\n\n| Type                       | Platform | Mechanism                                                   |\n|----------------------------|----------|-------------------------------------------------------------|\n| `obsidian-cli` (default)   | Desktop  | Obsidian `Command Line Interface` (`CLI`) (`obsidian eval`) |\n| `obsidian-cdp`             | Desktop  | Obsidian `Chrome DevTools Protocol` (`CDP`)                 |\n| `obsidian-android-appium`  | Mobile   | Obsidian Android Appium WebView JS injection                |\n\n#### `CLI` transport (default)\n\nShells out to the Obsidian `Command Line Interface` (`CLI`) binary for each eval call. This is the default when no `obsidianTransport` is configured.\n\n**Setup:**\n\n1. [Install the Obsidian `CLI`](https://obsidian.md/help/cli#Install+Obsidian+CLI).\n2. Enable `CLI` in Obsidian: `Settings → General → Developer tools → Enable CLI`.\n\nNo additional vitest configuration needed — `CLI` is the default transport.\n\n#### `CDP` transport\n\nConnects via WebSocket to Obsidian `Chrome DevTools Protocol` (`CDP`) endpoint. No `CLI` binary needed, no `CLI enabled` setting required, and lower overhead per eval.\n\n**Setup:**\n\n1. Launch Obsidian with the `--remote-debugging-port` flag:\n\n   ```powershell\n   # Windows (PowerShell)\n   \u0026 \"$env:LOCALAPPDATA\\Programs\\Obsidian\\Obsidian.exe\" --remote-debugging-port=8315\n   ```\n\n   ```cmd\n   # Windows (CMD)\n   \"%LOCALAPPDATA%\\Programs\\Obsidian\\Obsidian.exe\" --remote-debugging-port=8315\n   ```\n\n   ```bash\n   # macOS\n   /Applications/Obsidian.app/Contents/MacOS/Obsidian --remote-debugging-port=8315\n\n   # Linux\n   obsidian --remote-debugging-port=8315\n   ```\n\n2. Ensure [`Node.js`](https://nodejs.org/) 22+ is installed (uses built-in `WebSocket` and `fetch` globals).\n3. Configure vitest:\n\n   ```ts\n   // vitest.config.ts\n   export default defineConfig({\n     test: {\n       fileParallelism: false,\n       globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n       environmentOptions: {\n         obsidianTransport: { type: 'obsidian-cdp' },\n       },\n     },\n   });\n   ```\n\n   Optional configuration:\n\n   ```ts\n   environmentOptions: {\n     obsidianTransport: {\n       type: 'obsidian-cdp',\n\n       // default values can be omitted\n       host: 'localhost',\n       port: 8315,\n       commandTimeoutInMilliseconds: 30000\n     },\n   }\n   ```\n\n#### Obsidian Android Appium transport\n\nRuns tests against Obsidian Mobile on an Android emulator or real device via Appium WebView injection.\n\n**Setup:**\n\n1. Install [Android Studio](https://developer.android.com/studio), which includes the Android SDK and `adb` command-line tools\n\n2. Set up a device — either an emulator or a real device:\n\n   **Emulator:**\n\n   - Open Android Studio → Device Manager → Create Virtual Device\n   - Select a phone profile (e.g. Pixel 7) and a system image (e.g. API 34)\n   - Start the emulator\n\n   **Real device:**\n\n   - Enable Developer Options: Settings → About phone → tap \"Build number\" 7 times\n   - Enable USB Debugging: Settings → Developer options → USB debugging\n   - Connect via USB and accept the debugging prompt on the device\n   - Verify the device is detected:\n\n     ```bash\n     adb devices\n     ```\n\n3. Install [Obsidian](https://obsidian.md/download) on the device (via Play Store or APK sideload) and grant storage permission — either via the app's permission prompt or via `adb`:\n\n   ```bash\n   adb shell appops set md.obsidian MANAGE_EXTERNAL_STORAGE allow\n   ```\n\n4. Install [Appium](https://appium.io/) and the [UiAutomator2 driver](https://github.com/appium/appium-uiautomator2-driver):\n\n   ```bash\n   npm install -g appium\n   appium driver install uiautomator2\n   ```\n\n5. Start the Appium server:\n\n   ```bash\n   appium\n   ```\n\n6. Find the device ID (use the value from the `adb devices` output):\n\n   ```bash\n   adb devices\n   # Example output:\n   # emulator-5554   device\n   # R5CR1234567     device   ← real device\n   ```\n\n7. Configure vitest:\n\n   ```ts\n   // vitest.config.ts\n   export default defineConfig({\n     test: {\n       fileParallelism: false,\n       globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n       environmentOptions: {\n         obsidianTransport: {\n           type: 'obsidian-android-appium',\n           appiumUrl: 'http://localhost:4723',\n           deviceId: 'emulator-5554',\n         },\n       },\n     },\n   });\n   ```\n\n\u003e [!NOTE]\n\u003e\n\u003e Plugins with `isDesktopOnly: true` in `manifest.json` automatically reject Android tests.\n\n### Running multiple platforms\n\nUse vitest projects to run the same tests on multiple platforms:\n\n```ts\n// vitest.config.ts\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    projects: [\n      {\n        test: {\n          name: 'integration-tests:desktop-cli',\n          fileParallelism: false,\n          globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n          include: ['src/**/*.integration.test.ts'],\n          exclude: ['src/**/*.android.integration.test.ts'],\n          // default, can be omitted\n          environmentOptions: {\n            obsidianTransport: { type: 'obsidian-cdp' },\n          },\n        },\n      },\n      {\n        test: {\n          name: 'integration-tests:desktop-cdp',\n          fileParallelism: false,\n          globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n          include: ['src/**/*.integration.test.ts'],\n          exclude: ['src/**/*.android.integration.test.ts'],\n          environmentOptions: {\n            obsidianTransport: { type: 'obsidian-cdp' },\n          },\n        },\n      },\n      {\n        test: {\n          name: 'integration-tests:android-appium',\n          fileParallelism: false,\n          globalSetup: ['obsidian-integration-testing/vitest-global-setup'],\n          include: ['src/**/*.android.integration.test.ts'],\n          environmentOptions: {\n            obsidianTransport: {\n              type: 'obsidian-android-appium',\n              appiumUrl: 'http://localhost:4723',\n              deviceId: 'emulator-5554',\n            },\n          },\n        },\n      },\n    ],\n  },\n});\n```\n\nRun specific platforms:\n\n```bash\n# All tests\nnpx vitest run\n\n# Desktop CLI only\nnpx vitest run --project integration-tests:desktop-cli\n\n# Desktop CDP only\nnpx vitest run --project integration-tests:desktop-cdp\n\n# Android only (requires Appium + emulator running)\nnpx vitest run --project integration-tests:android-appium\n\n# All platforms\nnpx vitest run --project integration-tests:*\n```\n\n## Support\n\n\u003c!-- markdownlint-disable MD033 --\u003e\n\n\u003ca href=\"https://www.buymeacoffee.com/mnaoumov\" target=\"_blank\"\u003e\u003cimg src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" height=\"60\" width=\"217\"\u003e\u003c/a\u003e\n\n\u003c!-- markdownlint-enable MD033 --\u003e\n\n## My other Obsidian resources\n\n[See my other Obsidian resources](https://github.com/mnaoumov/obsidian-resources).\n\n## License\n\n© [Michael Naumov](https://github.com/mnaoumov/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnaoumov%2Fobsidian-integration-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmnaoumov%2Fobsidian-integration-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnaoumov%2Fobsidian-integration-testing/lists"}