Transport modes
The transport determines how the library talks to Obsidian. Configure it through your runner’s transport options (see Getting started).
| Type | Platform | Mechanism |
|---|---|---|
obsidian-cdp (default) |
Desktop | Obsidian Chrome DevTools Protocol (CDP) |
obsidian-android-appium |
Mobile | Obsidian Android Appium WebView injection |
This guide covers the desktop CDP transport; the mobile one has its own Android testing guide.
The owned CDP instance
Section titled “The owned CDP instance”By default the library launches and owns an isolated Obsidian instance in a temporary --user-data-dir on a free --remote-debugging-port, and talks to it over CDP. The owned instance never touches your real Obsidian — config, vault registry, running window and auto-update are all left alone — and it runs in parallel with your everyday Obsidian.
Nothing needs configuring; the owned CDP instance is the default:
export default defineConfig({ test: { fileParallelism: false, globalSetup: ['obsidian-integration-testing/vitest-global-setup-plugin'] }});The CLI server error line at boot is expected
Section titled “The CLI server error line at boot is expected”An owned instance prints this once on startup, on every run:
[obsidian-instance:stderr] CLI server error: Error: listen EADDRINUSE: address already in use \\.\pipe\obsidian-cli-<username>Obsidian serves its command-line interface on a named pipe (\\.\pipe\obsidian-cli-<username> on Windows, $XDG_RUNTIME_DIR/.obsidian-cli.sock elsewhere) whose name depends only on the user, not on the user-data dir. The owned instance always wins its own single-instance lock, so it always tries to serve that pipe — and whatever started first, normally your everyday Obsidian, already holds it.
Losing it costs the run nothing: Obsidian logs the failure and carries on, the harness never issues CLI commands, and every test talks CDP. Nothing needs configuring and the line is safe to ignore. It is not a symptom of two test runs colliding — a single run with nothing else going on prints it too.
Pin an Obsidian version
Section titled “Pin an Obsidian version”To run against a specific Obsidian version, set obsidianVersion and/or obsidianInstallerVersion. Each accepts an explicit 'x.y.z', 'public-latest', or 'catalyst-latest'. Downloaded asars and installer shells are cached under the system temp directory for reuse.
environmentOptions: { obsidianTransport: { type: 'obsidian-cdp', // The Obsidian app version (asar). At or above the installed shell version // it is applied as a fast asar swap; an older version transparently // downloads the matching installer. obsidianVersion: '1.8.10' }}obsidianVersionpins the app code (asar). When omitted, the owned instance runs the same version your installed Obsidian currently runs.obsidianInstallerVersionpins the Electron shell (installer build), downloaded and extracted from the matching GitHub release. Windows installers require 7-Zip onPATH. Public releases only — catalyst and beta builds have no public installer, so a catalyst version can only be pinned at the asar level.
To run the whole suite across the supported range instead of one pinned version, see Version matrix.
Dead-boot fast-fail
Section titled “Dead-boot fast-fail”If you pin an app version that cannot run on the launched Electron shell — an obsidianInstallerVersion too old for the obsidianVersion — Obsidian loads a black screen: the renderer finishes loading but the app never bootstraps (empty <body>, no window.app). Rather than waiting out the full readiness timeout, the harness detects this terminal state and throws a RendererFailedToInitializeError as soon as it has held for a short grace window:
import { RendererFailedToInitializeError } from 'obsidian-integration-testing';
try { // ... register a vault against an incompatible version pair} catch (error) { if (error instanceof RendererFailedToInitializeError) { // The installer/Electron version is too old for this Obsidian app version. }}deadBootGraceInMilliseconds(default10000) — how long the renderer must sit in the dead state (documentcomplete, empty<body>, nowindow.app) before fast-failing. The grace clock starts when the renderer first reportsreadyState: 'complete', so a slow-but-valid boot is never misjudged. Set0to disable the fast-fail and restore the plain wait-out-the-readiness-timeout behavior. Owned mode only; ignored when attaching.
Vaults whose config folder is not .obsidian
Section titled “Vaults whose config folder is not .obsidian”Obsidian lets a vault keep its settings in a folder other than .obsidian — Settings → About → Override config folder. Point the harness at such a vault and, by default, it opens against .obsidian instead. If the vault has a stale .obsidian sitting beside the real folder, that is not an error you will see: the vault opens, the layout is ready, and the run reports success against the wrong settings and none of the plugins you meant to test. Set configDirectory to name the real folder:
environmentOptions: { obsidianTransport: { type: 'obsidian-cdp', configDirectory: '.obsidian-desktop' }}configDirectory(defaultundefined, i.e. Obsidian’s own.obsidian) — the vault’s config folder. It must start with a dot, must not be the bare dot, and must not contain a path separator; anything else throws before Obsidian is launched. Owned mode only; ignored when attaching, where your own Obsidian opens the vault under its own config.
Obsidian stores this override in localStorage, not in any file, and reads it once — in the vault’s own renderer, while the vault is being set up. So the harness cannot write it into the vault’s window: by then the read has already happened. Setting configDirectory therefore changes how the owned instance boots. It comes up on Obsidian’s starter screen, the override is written in that renderer, and only then is the vault opened. That costs roughly one extra second and is why the option is opt-in rather than always on.
Because localStorage is scoped to a user-data directory and an owned instance gets a fresh temporary one, nothing is inherited from your own Obsidian — a vault you have configured this way in your day-to-day Obsidian still needs the option here.
Window visibility
Section titled “Window visibility”By default the owned Obsidian window is shown. Integration setup explicitly hides its owned window so test runs do not steal focus:
environmentOptions: { obsidianTransport: { type: 'obsidian-cdp', isObsidianAppVisible: false // hide the window for this run }}isObsidianAppVisible(defaulttrue) — whenfalse, the harness launches the owned instance with keep-alive Chromium flags and moves its window off-screen once Electron’s remote bridge is up. Off-screen (not minimized) keeps the renderer fully live, sosetTimeout,requestAnimationFrame,:hoverand trusted keyboard/pointer input behave exactly as they would for a visible window — tests are unaffected. Set it explicitly tofalsein any automated run that should not show a window. Ignored when attaching: the harness never moves your own running Obsidian.
Attach to a running Obsidian
Section titled “Attach to a running Obsidian”To attach to an already-running Obsidian instead of owning one, launch Obsidian with --remote-debugging-port=<port> and set port to that same port. The version-pinning options do not apply in attach mode.
# Windows (PowerShell) — uses Obsidian from PATH (e.g. scoop), falling back to the installer location$obsidian = (Get-Command Obsidian.exe -ErrorAction SilentlyContinue).Sourceif (-not $obsidian) { $obsidian = "$env:LOCALAPPDATA\Programs\Obsidian\Obsidian.exe" }Start-Process $obsidian -ArgumentList '--remote-debugging-port=8315'environmentOptions: { obsidianTransport: { type: 'obsidian-cdp', port: 8315, // must match the --remote-debugging-port Obsidian was launched with
// default values can be omitted host: 'localhost', commandTimeoutInMilliseconds: 30000 }}Run several platforms from one config
Section titled “Run several platforms from one config”Vitest projects let the same tests run on both transports:
import { defineConfig } from 'vitest/config';
export default defineConfig({ test: { projects: [ { test: { name: 'integration-tests:desktop-cdp', fileParallelism: false, globalSetup: ['obsidian-integration-testing/vitest-global-setup-plugin'], include: ['src/**/*.integration.test.ts'], exclude: ['src/**/*.android.integration.test.ts'], // default transport, can be omitted environmentOptions: { obsidianTransport: { type: 'obsidian-cdp' } } } }, { test: { name: 'integration-tests:android-appium', fileParallelism: false, globalSetup: ['obsidian-integration-testing/vitest-global-setup-plugin'], include: ['src/**/*.android.integration.test.ts'], environmentOptions: { obsidianTransport: { type: 'obsidian-android-appium', appiumUrl: 'http://localhost:4723', avdName: 'obsidian_test' } } } } ] }});# All testsnpx vitest run
# Desktop CDP onlynpx vitest run --project integration-tests:desktop-cdp
# Android only (requires Appium + emulator running)npx vitest run --project integration-tests:android-appium
# All platformsnpx vitest run --project integration-tests:*Related
Section titled “Related”ObsidianCdpTransportOptionsAPI referenceRendererFailedToInitializeErrorAPI reference- Ad-hoc debugging — the same knobs outside a test run.