Skip to content

Using with obsidian-typings

This package has no runtime dependency on obsidian-typings, but it works seamlessly alongside it.

obsidian-typings uses declare module 'obsidian' to augment the obsidian types with about a thousand internal members (App.commands, Menu.items, and so on). That makes import('obsidian').App a superset of what obsidian.d.ts declares on its own.

A mock member that implements one of those internals carries the internal’s real name. Menu.items is simply the mock’s own member, so code written against obsidian-typings runs against the mocks with no setup step, no configuration, and no property assignment:

import { Menu } from 'obsidian-test-mocks/obsidian';
const menu = Menu.create2__();
menu.addItem((item) => item.setTitle('Rename'));
console.log(menu.items.length); // 1

The __ suffix stays only on members that do not exist in Obsidian at all — factories (create__()), type bridges (asOriginalType__()), and test helpers (simulateClick__()). A real internal is not a fake member, so marking it as one would be a lie.

Class Members
AbstractInputSuggest selectCb, textInputEl
AbstractTextComponent changeCallback
App appId, changeTheme, fixFileLinks, getTheme, isMobile, plugins, setTheme, viewRegistry
BasesViewConfig setOrder
ButtonComponent setLoading
ColorComponent changeCallback, colorPickerEl
Component _children, _events, _loaded
DateValue date, printDate, printTime, time
DropdownComponent changeCallback
DurationValue days, hours, milliseconds, minutes, months, seconds, years
Editor cm
Events _
ExtraButtonComponent changeCallback
FileManager app
FileValue app, file, getBacklinks, getEmbeds, getLinks, getProps, getTags
FileView loadFile
LinkValue app, display, resolve, sourcePath
ListValue compare, data, earliest, flatten, getDates, getNumbers, join, latest, lazyEvaluator, max, mean, median, min, reverse, slice, sort, stddev, sum, unique
MarkdownEditView type, view
MarkdownPreviewView onload, type
MarkdownRenderer onload
MarkdownView editMode, getEphemeralState, modes, onload
Menu dom, items, setSectionSubmenu
MenuItem checked, disabled, section, setSubmenu, submenu
MetadataCache app, computeMetadataAsync, fileCache, isSupportedFile, iterateRefsForFile, metadataCache
Modal bgEl, headerEl
ObjectValue data, getInsensitive, lazyEvaluator, valuesRaw
PopoverSuggest isOpen
PrimitiveValue data
ProgressBarComponent progressBar
RegExpValue regexp
RenderContext app, renderExternalLink, renderFileLink, renderTag
Setting disabled, iconEl, rowClick, setAction, setIcon, setNavigable, setNoInfo, setRowClick, setVisibility
SettingGroup components, controlEl, groupEl, headerEl, headerInnerEl
SettingTab renderTab
SliderComponent changeCallback
SuggestModal instructionsEl
TAbstractFile deleted
TagValue tagMatches
TFile getShortName, name
TFolder getParentPrefix
ToggleComponent changeCallback, on
UrlValue display
Value icon, keys, objectAccess
Vault config, exists, fileMap, getAbstractFileByPathInsensitive, getAvailablePath, getAvailablePathForAttachments, getConfig, setConfig
View close, open
Workspace activeLeafEvents, activeTabGroup, app, createLeafInTabGroup, floatingSplit, getFocusedContainer, isAttached, iterateLeaves, lastActiveFile, lastTabGroupStacked, onLayoutChange, requestActiveLeafEvents, requestLayoutChangeEvents, requestUpdateLayout, splitLeaf, updateLayout
WorkspaceItem containerEl, detach, dimension, setDimension, setParent
WorkspaceLeaf _empty, activeTime, canNavigate, group, pinned, setDimension, tabHeaderCloseEl, tabHeaderEl, tabHeaderInnerIconEl, tabHeaderInnerTitleEl, tabHeaderStatusContainerEl, working
WorkspaceParent allowSingleChild, children, insertChild, removeChild, replaceChild
WorkspaceSidedock allowSingleChild, children, direction
WorkspaceSplit direction, setDirection
WorkspaceTabs currentTab, isStacked, selectTab, selectTabIndex, setStacked

app.plugins is modelled, and getPlugin() answers null. That is not a stub — a mock vault genuinely has no community plugins installed, so null is the truth about it:

const app = App.createConfigured__();
app.plugins.getPlugin('notebook-navigator'); // null

This matters because library code reads the registry on your behalf. obsidian-dev-utils, for instance, looks for Notebook Navigator’s optional menu API on layout ready, so a plugin whose own code never mentions app.plugins still touches it through what it inherits.

To model a vault that does have the plugin, seed it:

app.plugins.registerPlugin__('notebook-navigator', castTo<Plugin>({ api }));
app.plugins.getPlugin('notebook-navigator'); // the stand-in
app.plugins.enabledPlugins; // Set { 'notebook-navigator' }

The instance can be a full Plugin mock via asOriginalType2__(), or any stand-in carrying just the members under test. unregisterPlugin__(id) reverses it. Only that core is modelled — the enable/disable lifecycle, installing, updates and deprecation checks throw, as below.

app.plugins.manifests follows the same registrations: a registered instance that carries its own manifest (every Plugin mock does) is filed under its id, and unregistering drops it. A bare stand-in files nothing, so a test that needs the plugin’s name — obsidian-dev-utils’ resource lock reads app.plugins.manifests[id]?.name to say who holds a lock — assigns the entry directly:

app.plugins.manifests['notebook-navigator'] = manifest;

editor.cm is a real CodeMirror 6 EditorView, built on first read and kept in step with the mock editor in both directions. An edit, selection, undo or resetState__() made through the editor is dispatched into the view, and a transaction dispatched into the view is applied back to the editor as one change, undone in one step, carrying the selection CodeMirror landed on. So editor.cm.state.doc.toString() and editor.getValue() always agree, and library code that reconfigures the view — obsidian-dev-utils makes a locked editor read-only by appending a Compartment and reconfiguring it to EditorState.readOnly — runs against the real thing rather than failing on an unmocked cm.

The view is detached and never painted: its state is honest, its DOM is not.

app.internalPlugins and app.commands are still unmocked on purpose: neither has an honest empty state. Real Obsidian always ships core plugins with several enabled, and this package’s Plugin.addCommand records into the plugin’s own mock-only registry, so answering “none” for either would be a wrong answer rather than an empty vault.

Everything else obsidian-typings declares is deliberately not mocked, and reading it throws through the strict proxy:

const app = App.createConfigured__();
app.commands; // Error: Property "commands" is not mocked in App

That is the guarantee working, not a gap. A mock that answered undefined for a member it cannot model would turn a loud failure into a wrong test result — so a member is implemented only when there is real behavior or real state behind it, never as an inert stub.

The full split is recorded in scripts/obsidian-typings-unimplemented.json and enforced by src/obsidian/obsidian-typings-conformance.test.ts: every augmented member is either implemented above or listed there, so an obsidian-typings upgrade that introduces new members fails the suite until somebody decides about them.

Assign it before the code under test reads it:

import { App } from 'obsidian-test-mocks/obsidian';
const app = App.createConfigured__();
const original = app.asOriginalType__();
// Type-safe with obsidian-typings installed — no casts needed.
original.internalPlugins = { manifests: {} };

Without obsidian-typings, assign through a Record cast:

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

If you find yourself assigning the same member in every test, that is a good sign it should be implemented here instead — please open an issue.