obsidian/trusted-input
Trusted input helpers that work on either platform — the documented entry point for keystrokes, pointer moves and clicks that Obsidian actually believes.
Untrusted input (element.dispatchEvent(new MouseEvent('click'))) is silently ignored by CodeMirror, by the CSS :hover engine and by every e.isTrusted guard in Obsidian, so a test driving it exercises nothing while still passing. A trusted event has to come from outside the renderer, and how it gets there is platform-specific: Electron’s webContents.sendInputEvent on desktop, a CDP injection round-tripped through the harness’s host channel on mobile.
This module internalizes that split (see the project AGENTS.md L6): it stays cross-platform-loadable — no platform-only module is on its import graph — and each helper defers to a Platform-gated call-time dynamic import() of the arm that fits. So a consumer writes await clickElement({ element }) once and never a platform check.
The arms remain importable directly, for a caller making a deliberate platform commitment: desktop-trusted-input and mobile-trusted-input. Prefer this facade otherwise.
Every helper must be awaited. The desktop arm has nothing to wait for, but the mobile arm’s injection is a round-trip to the host, so a missing await lets the assertion run before the input has landed. The signature is the same on both platforms precisely so that a suite growing a mobile lane does not have to be rewritten.
Not every helper survives the port. moveMouse, hoverElement and unhoverElement throw on mobile: a touch screen has no persistent pointer and :hover has no touch equivalent. They throw rather than no-op, because a silent no-op is exactly the false confidence trusted input exists to end. Gate those on Platform.isDesktopApp, or drive the element with clickElement instead.
Functions
Section titled “Functions”| Function | Description |
|---|---|
| clickElement | Clicks the center of an element using trusted input — Electron’s sendInputEvent on desktop, a CDP touch tap in the WebView on mobile. The element-relative counterpart of clickMouse, mirroring the moveMouse / hoverElement split. Use 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. |
| clickMouse | Clicks at the given viewport coordinates using trusted input, so the renderer synthesizes a real click (or contextmenu, for the right button) with isTrusted === true. The low-level primitive: one click at one point, with no waiting for any effect — callers poll their own readiness signal. Prefer 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. 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. |
| hoverElement | Moves the pointer to the center of an element using trusted pointer input, then polls until the element actually matches :hover. Desktop only — it throws on mobile, where :hover has no touch equivalent. |
| moveMouse | Moves the pointer to the given viewport coordinates using a trusted pointer move, so :hover rules genuinely apply. The low-level primitive: a single move that does not wait for any state to settle. Prefer hoverElement / unhoverElement for element-relative moves. Desktop only — it throws on mobile, which has no persistent pointer to move. |
| pressKey | Presses a single key (optionally with modifiers) using trusted keyboard input, firing the full real key pipeline — keydown → keypress → beforeinput → input → keyup. It is delivered to the window’s DOM-focused element, so the caller focuses the target first and then awaits the expected effect; the press itself polls for nothing. On mobile the accepted keys are the named ones ('Enter', 'Escape', 'Tab', 'Backspace', 'Delete', the arrows) plus single printable characters — anything else throws rather than pressing nothing. |
| typeIntoEditor | Types text into a CodeMirror editor using trusted keyboard input. Focuses the editor (caret to the end) and presses every code point of the text — the same trusted sequence a real user produces — so the text reaches the document only if the editor genuinely holds focus. Then it polls until the document reflects the input, or a bounded timeout elapses (the expected outcome when the editor is read-only or focus was stolen). |
| unhoverElement | Moves the pointer to a point just outside an element’s bounding box using a trusted pointer move, then polls until the element no longer matches :hover. The inverse of hoverElement. When an element spans the full viewport (no point outside its box is reachable), use moveMouse directly to move the pointer to a known empty coordinate instead. Desktop only — it throws on mobile, for the same reason hoverElement does. |