Lib
The shared library bag injected into every evalInObsidian callback as CommonArguments.lib.
Two layers compose into this one bag. The base — the harness-provided renderer-driving helpers declared below (the trusted-input primitives and Lib.waitUntil) — is always present. On top, provider packages register a renderer-side resolver via registerLibResolver to Object.assign their whole renderer-safe library at runtime, and augment this augmentable interface (the i18next CustomTypeOptions idiom) via declare module 'obsidian-integration-testing' to type it. Multiple providers compose: their exports merge at runtime and their augmentations merge in the type system (interface Lib extends …).
Import:
import type { Lib } from 'obsidian-integration-testing';Example:
declare module 'obsidian-integration-testing' { interface Lib extends (typeof import('obsidian-dev-utils/__merged')) {}}Signature:
export interface LibProperties
| Property | Type | Description |
|---|---|---|
| clickElement | (this: void, params: ClickElementParams) => Promise<void> | Clicks the center of an element using **trusted** pointer input — Electron's sendInputEvent on desktop, a CDP touch tap in the WebView on mobile.The element-relative counterpart of Lib.clickMouse, mirroring the Lib.moveMouse / Lib.hoverElement split. Use Lib.clickMouse directly when the point to click is **not** the element's center — the markdown editor's margin, for instance, lies inside cm.scrollDOM but outside .cm-sizer, so no element's center lands on it.**Must be awaited.** On mobile the injection is a round-trip to the host — the renderer cannot produce a trusted event itself — so a missing await would let the assertion run before the click landed.**On mobile it waits for the element to stop moving, and refuses a covered one.** The tap reaches the page about a second after its point is read, and Obsidian Mobile slides a modal in with a transform transition that can hold for over a second, so a point read too early is where the control *used to be*. So the mobile path first waits (up to 5 s) until no finite animation or transition runs on the element or any ancestor and its box has held across two reads, then checks with elementFromPoint that the center belongs to the element (asked of the element's own root, so a target inside a shadow root is hit-tested in its own tree). It throws a named error in either case rather than tapping, because a trusted tap goes to whatever is on top and would otherwise land on it silently. Use Lib.clickMouse to tap a point deliberately, covered or not. Desktop is unchanged. |
| clickMouse | (this: void, params: ClickMouseParams) => Promise<void> | Clicks at the given web-contents coordinates using **trusted** Electron pointer input, so Chromium synthesizes a real click (or contextmenu, for the right button) with isTrusted === true.This is what element.dispatchEvent(new MouseEvent('click')) cannot do: Obsidian and CodeMirror gate on isTrusted, so a dispatched event silently exercises nothing while the test still passes whatever weaker assertion it makes. Obsidian 1.13.7's markdown viewport (margin) menu, for example, opens from a cm.scrollDOM contextmenu listener guarded by e.isTrusted, which a dispatched event never gets past.It is the low-level primitive: a single trusted mouseMove → mouseDown → mouseUp at one point, with no waiting for any effect (callers poll their own readiness signal). The leading move is what puts the pointer over the hit-test target before the button goes down. Prefer Lib.clickElement for element-relative clicks.A real context menu actually opens, so a suite driving a right click must close it (or remove the leftover .menu element) before the next test.It clicks in the main window unless ClickMouseParams.window names a popout. Lib.clickElement needs no such parameter: it clicks in the window that owns its element. **On mobile** the button model does not survive the port: touch has no buttons, so 'left' (the default) is a **tap**, 'right' is the **long-press** that opens Obsidian Mobile's context menu, and 'middle' throws — there is no gesture for it, and inventing one would be worse than saying so. The coordinates are CSS pixels in the WebView's own viewport, which is what CDP takes, so devicePixelRatio never enters the picture.**Must be awaited** — see Lib.clickElement. |
| createNote | (this: void, params: CreateNoteParams) => Promise<TFile> | Creates a note and does not return until its content is verifiably on disk, rewriting it if it is not. Use this instead of app.vault.create in any suite that may run on Android. The emulator transport loses roughly **0.9 %** of vault.create writes (measured: 7 lost in 800 creates): the file lands **0 bytes** on disk while Obsidian's in-memory TFile.stat reports the full byte count, and it does not heal on its own. A suite doing ~34 creates per run therefore has a ~26 % chance of at least one lost write, and whichever test loses that lottery fails on a waitUntil for content that was never written — which is why it reads as an unrelated per-test flake rather than one shared cause.Verification is by **reading the note back**, never by inspecting TFile.stat: stat is exactly the field that lies here. A rewrite through vault.modify with the same content lands correctly (also measured), so a lost write costs a retry rather than a failure. A note whose content still does not match after the bounded retries throws, naming the path and both lengths, so a genuinely broken write fails loudly instead of spinning.Harmless everywhere else — on a transport that does not lose writes the read-back matches first time and nothing is rewritten. |
| hoverElement | (this: void, params: HoverElementParams) => Promise<void> | Moves the mouse pointer to the center of an element using **trusted** Electron pointer input, then polls until the element actually matches :hover.Because the move is trusted (see Lib.moveMouse), the real :hover state takes effect in the CSS engine — real theme var() values and real compositing — instead of a hand-simulated cascade. It polls the live element.matches(':hover') state (not a fixed delay), so it is robust under shared-instance load. It targets the single shared window's **global** pointer, so only one element is hovered at a time.**Desktop only — it throws on mobile**, deliberately: touch input has no hover state, so there is nothing faithful to inject. A silent no-op would leave a test asserting against a hover that never happened, which is the exact false-confidence failure trusted input exists to prevent. Branch on Platform.isDesktopApp, or drive the element with Lib.clickElement. |
| moveMouse | (this: void, params: MoveMouseParams) => Promise<void> | Moves the mouse pointer to the given web-contents coordinates using a **trusted** Electron pointer move. A trusted move (injected via Electron's webContents.sendInputEvent) updates the real pointer state in the CSS engine, so :hover rules genuinely apply — unlike dispatchEvent(new MouseEvent('mouseover')), which is untrusted and never sets :hover. It targets the single shared window's **global** pointer, so only one element is hovered at a time.This is the low-level primitive: it performs a single move and does **not** wait for any state to settle (callers poll their own readiness signal). Prefer Lib.hoverElement / Lib.unhoverElement for element-relative moves; use moveMouse directly when an element-relative target does not fit (e.g. an element spanning the full viewport width).**Desktop only — it throws on mobile**, for the same reason as Lib.hoverElement: there is no touch pointer to move. It moves the pointer in the main window unless MoveMouseParams.window names a popout. **Must be awaited** — see Lib.clickElement. |
| openSettingsTab | (this: void, params: OpenSettingsTabParams) => Promise<string[]> | Opens Obsidian's settings modal on a given tab, and does not return until that tab has actually rendered. **This exists because app.setting.open() on its own does not work from a test.** app.setting.containerEl is built at startup but is never in the document, and open() does not attach it — so the modal builds into a detached tree, open() returns without throwing, and a screenshot taken afterwards shows the untouched document. That is what made the settings tab look impossible to capture, and got recorded as such in two plugins.The fix is one step, and its **order is load-bearing**: the container is appended to document.body **before** open(). Attaching afterwards is too late — whatever the modal drew on open has already gone into the detached container, so it ends up on screen showing the wrong thing. That failure looks like success until the captured frame is examined, which is the trap this helper exists to remove.Re-attaching is idempotent, so a tab closed with app.setting.close() can simply be re-opened. |
| pressKey | (this: void, params: PressKeyParams) => Promise<void> | Presses a single key (optionally with modifiers) using **trusted** Electron keyboard input, firing the full real key pipeline — keydown → keypress → beforeinput → input → keyup.This is the key-press analog of Lib.typeIntoEditor: it injects a trusted keyDown → char → keyUp sequence via Electron's webContents.sendInputEvent, so it is delivered to the window's DOM-focused element and flows through the real input pipeline — unlike dispatchEvent(new KeyboardEvent(...)), which is untrusted (isTrusted: false) and ignored by CodeMirror and most key handlers. Use it for special keys ('Enter', 'Escape', 'Tab', arrow keys) and modifier combinations (Shift+Enter, Ctrl+A) that Lib.typeIntoEditor (which types printable text) does not cover.This is the low-level primitive: it injects the key press and does **not** poll for any effect (a key press has no universal observable outcome — Enter edits the document, Escape closes a modal, ArrowDown moves the selection). The caller focuses the intended target first, then awaits the expected effect via Lib.waitUntil. It targets the single shared window's **global** focus, so only the DOM-focused element receives the key.It presses the key in the main window unless PressKeyParams.window names a popout. Each Obsidian window is its own web contents, so a key pressed in one never reaches another. **On mobile** the same sequence is injected through the WebView's debugger ( Input.dispatchKeyEvent), which is equally trusted. Named keys ('Enter', 'Escape', 'Tab', 'Backspace', 'Delete', the arrows) and single printable characters are supported; any other multi-character name throws rather than pressing nothing.**Must be awaited** — see Lib.clickElement. |
| typeIntoEditor | (this: void, params: TypeIntoEditorParams) => Promise<void> | Types text into a CodeMirror Editor using **trusted** Electron keyboard input.Typing is pressing each character key in turn: this focuses the editor (caret to end) and presses every code point of text via Lib.pressKey — the same trusted keyDown → char → keyUp a real user produces. Each keystroke is delivered to the window's DOM-focused element and flows through CodeMirror's real input pipeline, so the typed text reaches the document **only if the editor genuinely holds focus**. This makes "the user typed into the editor" a faithful end-to-end check, unlike dispatchEvent(new KeyboardEvent(...)) (untrusted — ignored by CodeMirror) or execCommand('insertText') (mutates the selection even when the editor is not focused, masking focus bugs as false positives).After injecting the keystrokes it polls until the document reflects the input, or a bounded timeout elapses (the expected outcome when the editor is read-only and rejects the input, or when focus was stolen). |
| unhoverElement | (this: void, params: UnhoverElementParams) => Promise<void> | Moves the mouse pointer to a point just outside an element's bounding box using a **trusted** Electron pointer move, then polls until the element no longer matches :hover.The inverse of Lib.hoverElement. It targets the single shared window's **global** pointer, so only one element is hovered at a time. When an element spans the full viewport (no point outside its box is reachable), use Lib.moveMouse directly to move the pointer to a known empty coordinate instead. **Desktop only — it throws on mobile**, for the same reason as Lib.hoverElement. |
| waitUntil | (this: void, params: WaitUntilParams) => Promise<void> | Polls a predicate until it becomes truthy, or rejects once a bounded timeout elapses. Integration-test evalInObsidian callbacks routinely need to wait for an asynchronous effect to settle (a view to open, a DOM node to appear, a setting to apply). Because the callback is serialized via toString() and cannot import modules, it can't reuse obsidian-dev-utils' retryWithTimeout / runWithTimeout. This helper is the shared, injected replacement for the per-closure poll loops consumers would otherwise hand-roll.The predicate may be synchronous or asynchronous — it is awaited on every poll. It is checked immediately, then re-checked every intervalInMilliseconds until it returns truthy or timeoutInMilliseconds elapses, at which point the returned Promise rejects (the error includes message when provided). |
Links to this page: