End-to-end Testing
The toolkit supports three stacks that test a plugin inside a real Obsidian. All three stay supported, and a plugin can use more than one.
| Stack | Test style | What the toolkit provides |
|---|---|---|
obsidian-integration-testing |
Vitest; a closure runs inside Obsidian with evalInObsidian |
defineObsidianPluginVitestConfig, the desktop and Android projects, the harness plugin |
wdio-obsidian-service |
WebdriverIO with mocha; the test drives the UI from outside | defineWdioObsidianConfig, testEndToEndWithWdioObsidian |
obsidian-test |
node:test; the test is bundled into the plugin and runs inside Obsidian |
testEndToEndWithObsidianTest |
Which one to use
Section titled “Which one to use”Start with obsidian-integration-testing. It is what the testing guide sets up, it runs on desktop and on Android from the same test file, its tests are Vitest tests like the unit tests, and every @mnaoumov/obsidian-lib helper is reachable inside a test.
Add wdio-obsidian-service when you need to test several Obsidian versions in one run. It downloads each version into a cache, so appVersions: ['earliest', 'latest'] tests your minAppVersion and the current release side by side, on any machine and in CI, without Obsidian being installed. Its tests also drive the real UI through WebDriver.
Use obsidian-test when you want the tests compiled into the plugin itself and run with node:test. It is beta software, and it launches the Obsidian installed on the machine.
wdio-obsidian-service
Section titled “wdio-obsidian-service”Install the packages:
npm install --save-dev @wdio/cli @wdio/local-runner @wdio/mocha-framework @wdio/globals wdio-obsidian-service wdio-obsidian-reporterwdio.conf.mts is one call:
import { defineWdioObsidianConfig } from '@mnaoumov/obsidian-dev-toolkit/test-runners/wdio-obsidian';
export const config: WebdriverIO.Config = defineWdioObsidianConfig({ appVersions: ['earliest', 'latest'] });defineWdioObsidianConfig derives the configuration from the plugin’s layout:
- The plugin under test is the production build in
dist/build.pluginFolderchanges it. - The vault is the plugin’s demo vault (
demo-vault/) when there is one, and an empty vault otherwise. The service opens a copy, so the tests never change the folder.vault: 'test/vaults/simple'names another vault, andvault: falseforces the empty one. - The specs are
e2e/**/*.spec.ts.specschanges them. - Each entry of
appVersionsis one capability, so each version runs the whole suite. It defaults to['latest'].earliestis theminAppVersionofmanifest.json, andlatest-betaneeds an Obsidian Insiders account (OBSIDIAN_EMAILandOBSIDIAN_PASSWORD). So doesearliestwhenminAppVersionnames a version that was only ever a beta, such as1.5.0: set it to a public release. - Obsidian versions are downloaded into
.obsidian-cache/, which belongs in.gitignore.cacheFolderchanges it. - mocha gets a 60 s per-test timeout (
testTimeoutInMilliseconds), since Obsidian starts once per spec file.installerVersionandmaxInstancespass through to the service and WebdriverIO.
The result is plain data, so anything else is changed by spreading it: { ...defineWdioObsidianConfig(), logLevel: 'info' }.
Add @wdio/globals/types, @wdio/mocha-framework and wdio-obsidian-service to the types of the tsconfig.json that covers e2e/. A spec then reads:
import { browser, expect } from '@wdio/globals';
describe('my plugin', () => { it('runs its command', async () => { await browser.executeObsidianCommand('my-plugin:open-sample-modal'); await expect(browser.$('.modal-container .modal-content')).toExist(); });});The test:e2e script builds the plugin and runs WebdriverIO:
import { wrapCliTask } from '@mnaoumov/typescript-dev-toolkit/cli-utils';import { testEndToEndWithWdioObsidian } from '@mnaoumov/obsidian-dev-toolkit/test-runners/wdio-obsidian';
await wrapCliTask(() => testEndToEndWithWdioObsidian());It runs npm run build first, so the tests always load the current dist/build (shouldBuild: false skips it), then wdio run with wdio.conf.mts or wdio.conf.ts, whichever exists first (configPath names another). A missing package is reported with the npm install line that adds it.
obsidian-test
Section titled “obsidian-test”Install the package:
npm install --save-dev obsidian-testA test is a node:test test inside embedded(), which runs it only inside Obsidian:
import assert from 'node:assert';import test from 'node:test';import { embedded } from 'obsidian-test';
embedded((app) => { void test('the workspace is ready', () => { assert.ok(app.workspace); });});The test:e2e script:
import { wrapCliTask } from '@mnaoumov/typescript-dev-toolkit/cli-utils';import { testEndToEndWithObsidianTest } from '@mnaoumov/obsidian-dev-toolkit/test-runners/obsidian-test-runner';
await wrapCliTask(() => testEndToEndWithObsidianTest());obsidian-test bundles the plugin together with the test files, installs it into a temporary vault, launches Obsidian and reports the results. It replays its cached results when nothing changed, and shouldUseCache: false runs the tests anyway.
The runner restricts the test files to ./e2e/**/*.spec.ts. obsidian-test’s own default matches every *.test.ts too, which would bundle the plugin’s Vitest unit tests into Obsidian. testPattern changes the glob.
It launches Obsidian with the obsidian command. When that is not on PATH, name the executable with obsidianCommand. Do not point vaultPath at the demo vault: obsidian-test installs the plugin into the vault it is given.
obsidian-test 0.1.2 does not run on Windows. Its bundle step fails with Could not resolve "./runners/dynamic.js", because it matches a module path against a /-separated suffix, and on Windows the path has \ separators (obsidian-test#7). Run it on macOS or Linux, for example in CI, until a release fixes it.
obsidian-test looks for Obsidian’s vault list only at ~/.config/obsidian/obsidian.json, the Linux location. Elsewhere, set the OBSIDIAN_CONFIG_PATH environment variable to that file, for example ~/Library/Application Support/obsidian/obsidian.json on macOS or %APPDATA%\obsidian\obsidian.json on Windows. Without it, the run fails after the bundle step with Obsidian config path "..." does not exist.