npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

obsidian-integration-testing

v1.1.2

Published

Simplifies integration testing of Obsidian plugins.

Readme

obsidian-integration-testing

Buy Me a Coffee npm version npm downloads GitHub release Coverage: 100%

A set of helpers that simplify integration testing of Obsidian plugins against a running Obsidian instance via the Obsidian CLI.

Prerequisites

Installation

npm install --save-dev obsidian-integration-testing

Usage

Plugin Vitest setup

This entry point is designed for Obsidian plugin repos only. It 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 via the Obsidian CLI.

Add it as a Vitest global setup:

// vitest.config.ts
export default defineConfig({
  test: {
    globalSetup: ['obsidian-integration-testing/obsidian-plugin-vitest-setup']
  }
});

Write integration tests

Use evalInObsidian() to run code inside the Obsidian process. The vaultPath is optional — it defaults to process.cwd():

import { evalInObsidian } from 'obsidian-integration-testing';

// Simple expression
const sum = await evalInObsidian({
  args: { a: 2, b: 3 },
  fn: ({ a, b }) => a + b
});
// sum === 5

Access the Obsidian API

Every callback receives app (the Obsidian App instance) and obsidianModule (the full obsidian module):

// Read the vault config directory
const configDir = await evalInObsidian({
  fn: ({ app }) => app.vault.configDir
});

// Use the obsidian module
const yaml = await evalInObsidian({
  fn: ({ obsidianModule }) => obsidianModule.stringifyYaml({ key: 'value' })
});

// Access internal APIs
const title = await evalInObsidian({
  fn: ({ app }) => app.title
});

Pass complex arguments

Arguments are JSON-serialized. You can even pass functions — they are serialized via toString():

const result = await evalInObsidian({
  args: {
    transform(x: number): number {
      return x * 2;
    },
    value: 5
  },
  fn: ({ transform, value }) => transform(value)
});
// result === 10

Persist non-serializable values across calls

Obsidian 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:

import type { TFile } from 'obsidian';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { ContextId, evalInObsidian } from 'obsidian-integration-testing';

interface Context {
  file: TFile;
}

const contextId = new ContextId<Context>();

beforeEach(async () => {
  await evalInObsidian({
    contextId,
    fn: async ({ app, context }) => {
      context.file = await app.vault.create('test.md', '# Hello');
    }
  });
});

afterEach(async () => {
  await evalInObsidian({
    contextId,
    fn: async ({ app, context: { file } }) => {
      await app.vault.delete(file);
    }
  });
  await contextId.dispose();
});

it('should read the file path', async () => {
  const path = await evalInObsidian({
    contextId,
    fn: ({ context: { file } }) => file.path
  });
  expect(path).toBe('test.md');
});

Create a temporary vault

Use TempVault to create a disposable vault pre-populated with files:

import type { TFile } from 'obsidian';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { ContextId, evalInObsidian, TempVault } from 'obsidian-integration-testing';

interface Context {
  file: TFile;
}

const vault = new TempVault();

vault.populate({
  'note.md': '# Hello',
  'folder/nested.md': 'nested content',
});

const contextId = new ContextId<Context>();

beforeAll(async () => {
  await vault.register();

  // Resolve the pre-populated file into a TFile and store it in the context
  await evalInObsidian({
    contextId,
    fn: async ({ app, context }) => {
      const file = app.vault.getFileByPath('note.md');
      if (!file) {
        throw new Error('File not found');
      }
      context.file = file;
    },
    vaultPath: vault.path
  });
});

afterAll(async () => {
  await contextId.dispose(vault.path);
  await vault.dispose();
});

it('should read a pre-populated file', async () => {
  const content = await evalInObsidian({
    fn: ({ app }) => app.vault.adapter.read('note.md'),
    vaultPath: vault.path
  });
  expect(content).toBe('# Hello');
});

it('should access the TFile from context', async () => {
  const path = await evalInObsidian({
    contextId,
    fn: ({ context: { file } }) => file.path,
    vaultPath: vault.path
  });
  expect(path).toBe('note.md');
});

Both TempVault and ContextId implement AsyncDisposable, so you can use await using for automatic cleanup.

Parent directories are created automatically. To create an empty folder, use a path ending with / and an empty string as content.

Test your plugin

Use getTempVault() to get the temporary vault created by the global setup:

import { describe, expect, it } from 'vitest';
import { evalInObsidian } from 'obsidian-integration-testing';
import { getTempVault } from 'obsidian-integration-testing/obsidian-plugin-vitest-setup';

describe('my-plugin', () => {
  const vault = getTempVault();

  it('should be enabled', async () => {
    const isEnabled = await evalInObsidian({
      args: { pluginId: 'my-plugin' },
      fn: ({ app, pluginId }) => app.plugins.enabledPlugins.has(pluginId),
      vaultPath: vault.path
    });
    expect(isEnabled).toBe(true);
  });

  it('should create a file', async () => {
    await evalInObsidian({
      fn: async ({ app }) => {
        await app.vault.create('test.md', '# Hello');
      },
      vaultPath: vault.path
    });

    const content = await evalInObsidian({
      fn: ({ app }) => app.vault.adapter.read('test.md'),
      vaultPath: vault.path
    });
    expect(content).toBe('# Hello');
  });
});

[!WARNING]

Parallelism:

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:

// vitest.config.ts
export default defineConfig({
  test: {
    fileParallelism: false
  }
});

 

[!WARNING]

evalInObsidian limitations:

  • The function is serialized via toString() and executed in a separate process. It must be self-contained — closures over local variables will not work.
  • 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.
  • The return value must also be JSON-serializable. You cannot return functions, class instances, Map, Set, DOM elements, or other non-serializable values.
  • Imports (import/require) are not available inside the function. Use obsidianModule to access the obsidian API, and app to access the Obsidian App instance.

Accessing internal APIs

Since 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:

1. Use obsidian-typings (recommended) — install obsidian-typings which declares the full internal API. Everything compiles with no extra work:

// With obsidian-typings installed — no casts needed
const title = await evalInObsidian({
  fn: ({ app }) => app.title
});

2. Manual module augmentation — declare only what you need:

declare module 'obsidian' {
  interface App {
    title: string;
  }
}

const title = await evalInObsidian({
  fn: ({ app }) => app.title
});

3. as any / @ts-expect-error / @ts-ignore (not recommended) — suppresses all type checking and hides real errors:

const title = await evalInObsidian({
  // @ts-expect-error -- accessing internal API
  fn: ({ app }) => app.title
});

// or
const title2 = await evalInObsidian({
  fn: ({ app }) => (app as any).title
});

Support

My other Obsidian resources

See my other Obsidian resources.

License

© Michael Naumov