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.
Real names, no bridging
Section titled “Real names, no bridging”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); // 1The __ 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.
What the mocks implement
Section titled “What the mocks implement”| 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 |
The plugin registry
Section titled “The plugin registry”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'); // nullThis 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-inapp.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;The editor’s CodeMirror view
Section titled “The editor’s CodeMirror view”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.
What still throws
Section titled “What still throws”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 AppThat 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.
Working with an unmocked member
Section titled “Working with an unmocked member”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.