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-test-mocks

v1.0.11

Published

Comprehensive test mocks for the Obsidian API.

Readme

obsidian-test-mocks

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

Comprehensive test mocks for the Obsidian plugin API. Provides in-memory implementations of every class and function in obsidian.d.ts, plus prototype extensions Obsidian adds to DOM/JS builtins. The package is tested with 100% code coverage (lines, branches, functions, and statements) enforced on every build.

Installation

npm install --save-dev obsidian-test-mocks

Peer dependencies: obsidian

Entry Points

| Import path | Description | | ------------------------------ | ------------------------------------------------------------------------- | | obsidian-test-mocks/obsidian | Mocks for every class/function in obsidian.d.ts | | obsidian-test-mocks/setup | Prototype extensions (HTMLElement, Document, Array, String, etc.) |

Usage with Vitest

In your vitest.config.ts, alias the obsidian module to the mock entry point:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    alias: {
      obsidian: 'obsidian-test-mocks/obsidian',
    },
    setupFiles: ['obsidian-test-mocks/setup'],
  },
});

Creating Mock Instances

Classes whose constructors are not public in obsidian.d.ts expose a static create__() factory method:

import { App } from 'obsidian';

const app = App.create__();

The __ suffix signals this member is not part of the real Obsidian API — it exists only in the mocks for testing purposes. This convention applies to all mock-only public members: factory methods (create__()), type-bridge helpers (asOriginalType__()), and test helpers (simulateClick__(), simulateChange__()).

Spying on Instance Creation

The create__() pattern makes all instance creation spyable:

import { vi } from 'vitest';
import { WorkspaceLeaf } from 'obsidian';

const spy = vi.spyOn(WorkspaceLeaf, 'create2__');

// ... code that creates leaves ...

expect(spy).toHaveBeenCalledTimes(2);

Pre-configured App

Use App.createConfigured__() for a fully wired App instance. Parent folders are created automatically from file paths:

import { App } from 'obsidian';

const app = App.createConfigured__({
  files: {
    'notes/daily/2024-01-01.md': '# New Year',
  },
});
// folders "notes" and "notes/daily" are created automatically

Paths ending with / are treated as folders (content must be empty):

const app = App.createConfigured__({
  files: {
    'archive/2023/': '',
  },
});

Strict Mocks

Every mock instance is wrapped in a Proxy that throws a descriptive error when you access a property that isn't implemented, instead of silently returning undefined:

Property "internalPlugins" is not mocked in App. To override, assign a value first: mock.internalPlugins = ...

Overriding Behavior

The strict proxy is fully override-friendly. Assign a value and subsequent reads just work:

// Spy on an existing method
vi.spyOn(app.vault, 'read').mockResolvedValue('custom content');

// Batch-extend with Object.assign
Object.assign(app, { commands: { addCommand: vi.fn() } });

Accessing Unimplemented Properties

Properties not implemented in the mock (such as app.internalPlugins) will throw at runtime:

Property "internalPlugins" is not mocked in App. To override, assign a value first: mock.internalPlugins = ...

You can add them by assigning a value first:

app.internalPlugins = { manifests: {} };

But since internalPlugins is not declared in obsidian.d.ts, TypeScript won't compile that assignment. 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. The assignment compiles with no extra work.

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

declare module 'obsidian' {
  interface App {
    internalPlugins: { manifests: Record<string, unknown> };
  }
}

app.internalPlugins = { manifests: {} };

3. Cast to Record<string, unknown> (less recommended) — quick one-off escape hatch, still catches typos in the value:

(app as Record<string, unknown>).internalPlugins = { manifests: {} };

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

(app as any).internalPlugins = { manifests: {} };

// @ts-expect-error -- accessing internal API
app.internalPlugins = { manifests: {} };

Type Bridging with asOriginalType__()

Mock types and original obsidian types are structurally different — you cannot assign a mock App to a parameter typed as import('obsidian').App. Every mock class provides an asOriginalType__() method that returns the instance typed as its original obsidian counterpart:

import type { App as AppOriginal } from 'obsidian';
import { App } from 'obsidian';

const app = App.createConfigured__();

// Pass to code that expects the original obsidian type
function pluginInit(app: AppOriginal): void { /* ... */ }
pluginInit(app.asOriginalType__());

This is a zero-cost type cast at runtime — no wrapping, no cloning. The __ suffix signals it is not part of the real Obsidian API.

Subclasses use numbered variants following inheritance depth: asOriginalType__() on the root class, asOriginalType2__() on its child, asOriginalType3__() on the grandchild, etc. The inherited base method remains callable at any level. For example, Vault (which extends Events) uses asOriginalType2__().

Reverse Bridging with fromOriginalType__()

The inverse of asOriginalType__(). Every mock class provides a static fromOriginalType__() method that accepts a real-typed obsidian object and returns it typed as the mock class:

import type { App as AppOriginal } from 'obsidian';
import { App, Vault } from 'obsidian';

const app: AppOriginal = App.createConfigured__().asOriginalType__();

// Convert back to mock type when you need mock-specific APIs
const mockVault = Vault.fromOriginalType2__(app.vault);
mockVault.setVaultAbstractFile__('path', file);

This eliminates the need for maintaining dual variables (mockApp / app). Keep the real App type throughout your test and convert to the mock type only when calling mock-specific APIs.

Subclasses use numbered variants following the same inheritance-depth convention as asOriginalType__(): fromOriginalType__() on the root, fromOriginalType2__() on its child, etc.

Because every mock is a strict mock, passing the result to code that accesses internal members (not part of obsidian.d.ts) will throw a descriptive error unless you assign those members first:

const app = App.createConfigured__();
const original = app.asOriginalType__();

// If pluginInit() accesses app.internalPlugins internally, this throws:
//   Property "internalPlugins" is not mocked in App.
//   To override, assign a value first: mock.internalPlugins = ...
pluginInit(original);

// Fix: assign the missing member before calling
(app as Record<string, unknown>)['internalPlugins'] = { manifests: {} };
pluginInit(original); // works

Overriding Exported Variables

Some exports like apiVersion are plain strings, not functions. Since ES module bindings are read-only for consumers, use vi.mock() to override them:

import { vi } from 'vitest';

vi.mock('obsidian', async (importOriginal) => ({
  ...(await importOriginal<typeof import('obsidian')>()),
  apiVersion: '1.8.0',
}));

import { apiVersion } from 'obsidian';

it('uses the overridden apiVersion', () => {
  expect(apiVersion).toBe('1.8.0');
});

Using with obsidian-typings

This package does not depend on obsidian-typings, but it works seamlessly if your project uses it.

obsidian-typings uses declare module 'obsidian' to augment obsidian types with dozens of internal properties (e.g., App.internalPlugins, App.commands). This makes import('obsidian').App a superset of what obsidian.d.ts alone declares. The mock types only implement the public API from obsidian.d.ts, so the two are structurally incompatible.

Use asOriginalType__() to bridge the gap when passing mocks to code that expects obsidian types:

import type { App as AppOriginal } from 'obsidian';
import { App } from 'obsidian';

function myPluginHelper(app: AppOriginal): void { /* ... */ }

const app = App.createConfigured__();
myPluginHelper(app.asOriginalType__());

With obsidian-typings installed, the returned type includes all augmented properties, so you can assign internal members in a type-safe way:

const app = App.createConfigured__();
const original = app.asOriginalType__();

// Type-safe with obsidian-typings — no casts needed
original.internalPlugins = { manifests: {} };

Without obsidian-typings, you can still assign them via a Record cast:

const app = App.createConfigured__();
(app as unknown as Record<string, unknown>)['internalPlugins'] = { manifests: {} };

Remember that accessing any property not assigned on the mock will throw a strict mock error at runtime, regardless of whether obsidian-typings makes it compile.

Design Principles

  • Only obsidian.d.ts — mocks expose exactly the public API, nothing extra
  • Meaningful implementations — real in-memory behavior (state tracking, callbacks, data storage), not empty stubs
  • Spyable — all instance creation routes through create__() so vi.spyOn() works everywhere
  • No obsidian-typings dependency — type shapes are inlined to avoid global module augmentation side effects

Support

My other Obsidian resources

See my other Obsidian resources.

License

© Michael Naumov