daledesilva_obsidian_projec.../docs/testing.md

11 KiB

Testing

This document describes the test setup for the Project Browser Obsidian plugin.

Why it exists

The test setup enables automated verification of logic, migrations, and UI behaviour without manually exercising the plugin in Obsidian. Unit and component tests run quickly and can be executed in CI. E2E tests run the plugin inside real Obsidian to verify commands, views, and integration.

Overview

  • Unit tests — Jest with jsdom, for utilities, migrations, and logic.
  • Component tests — React Testing Library for React components (when added).
  • E2E tests — WebdriverIO + wdio-obsidian-service, run plugin in Obsidian.
  • Manual QAnpm run open-qa to build and open an isolated Obsidian instance with a test vault.
  • CI — GitHub Actions runs unit and E2E tests on push/PR to main.

Running tests

npm run test:unit    # Run all unit tests (with coverage)
npm test             # Same as test:unit
npm run test:unit:watch   # Watch mode for development

npm run test:e2e           # Build and run all E2E specs (latest + beta when available)
npm run test:e2e:spec -- tests/e2e/commands.e2e.ts   # Run a single spec
npm run test:e2e:versions  # Show which Obsidian version(s) will be tested
npm run test:e2e:latest     # E2E against latest stable only
npm run test:e2e:beta       # E2E against latest-beta only (requires Insiders credentials)

Test layout

  • Colocated testssrc/**/*.test.ts and src/**/*.test.tsx next to the code they test.
  • Setup and mockstests/setupTests.ts and tests/__mocks__/.
  • Component test helperstests/helpers/component-test-utils.ts.

Mock layout

File Purpose
tests/__mocks__/styleMock.js Empty object for CSS/SCSS imports
tests/__mocks__/fileMock.js Stub for SVG and other assets
tests/__mocks__/obsidianMock.js Stubs for Obsidian API (TFile, TFolder, Menu, Notice, etc.)
tests/__mocks__/mainMock.js Minimal plugin stub for import from "src/main"

The Jest moduleNameMapper routes obsidian, src/main, and asset imports to these mocks so tests run without the real Obsidian environment.

Component test pattern

For React components that use Jotai or call getGlobals():

import { render } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { setGlobalsForComponentTest, makePlugin } from 'tests/helpers/component-test-utils';

const wrap = (ui: React.ReactElement) =>
  render(<JotaiProvider>{ui}</JotaiProvider>);

it('renders container', () => {
  setGlobalsForComponentTest();
  wrap(<SomeComponent plugin={makePlugin()} />);
  expect(document.querySelector('.some-class')).toBeInTheDocument();
});

Use makePlugin(), makeTFile(), and makeTFolder() from tests/helpers/component-test-utils.ts for consistent test data.

Manual QA

npm run open-qa           # Build, install into qa-test-vault, open isolated Obsidian
npm run open-qa-verbose   # Same, with Obsidian verbose logging

Manual QA uses obsidian-launcher (like E2E tests) to launch a sandboxed Obsidian instance — separate from your system installation, so it opens only the QA vault and does not affect your normal vaults. The qa-test-vault/ directory is created and populated; the built plugin is installed automatically. The vault is in .gitignore.

CI

The .github/workflows/test.yaml workflow runs on push and pull requests to main/master:

  1. Checkout, Node 22, npm ci
  2. npm run build
  3. npm run test:unit
  4. Cache Obsidian (from version resolution)
  5. Setup virtual display (Xvfb + herbstluftwm on Ubuntu)
  6. npm run test:e2e

Secrets OBSIDIAN_EMAIL and OBSIDIAN_PASSWORD (GitHub repository secrets) are recommended for stable, and required for beta. For local E2E, use .env or export these variables.

E2E test vault

E2E uses the generated vault at qa-test-vault/. The vault is created by qa-test-vault/generate.mjs at runtime — no vault content is committed. See qa-test-vault/README.md for generate and download-test-plugins usage.

E2E pipeline: npm run builddownload-test-plugins.shgenerate.mjswdio run

E2E test layout

  • Specstests/e2e/**/*.e2e.ts
  • Helperstests/e2e/helpers/dismiss-popups.ts (fallback for blocking notices), tests/e2e/helpers/open-card-browser.ts (openCardBrowserFrom, openCardBrowserAndEnterFolder), tests/e2e/helpers/project-browser-startup.ts (workspace reset + startup trigger helpers for reliability scenarios)
  • Vaultqa-test-vault/ (generated by generate.mjs, not committed)

E2E test coverage

Spec Covers
commands.e2e.ts Plugin loaded; open-project-browser command; folder sections at root; navigate into folder and see state sections + note cards; cycle-state-forward / cycle-state-backward (active note state); toggle-state-menu (state menu visibility). Ribbon icon open is not asserted (manual QA).
navigation.e2e.ts Back button returns to root; breadcrumb root click returns to root; opening a note card opens the note in the same leaf.
search.e2e.ts Search button shows search input; typing filters cards; clear button hides search and clears filter.
new-tab-replacement.e2e.ts Startup reliability coverage for all browser-entry paths: opening a new empty tab, clicking the ribbon button, closing all tabs including the final active tab, alternating entry methods, and recovering when the active leaf becomes empty without an active-leaf switch. Each scenario is repeated from multiple starting states.
settings.e2e.ts Open app settings, open Project Browser tab, assert settings section (e.g. .ddc_pb_section) is present.
file-types.e2e.ts File Types Test folder shows multiple file-type cards; Project A shows non-MD files (e.g. sample.png, sample.pdf) in the section.
context-menus-modals.e2e.ts Folder context menu on right-click (expected items); file context menu on right-click on note card; Rename folder from menu opens modal; cancel leaves vault unchanged.

All E2E specs use the same generated vault and call dismissBlockingPopups() in before. Startup-focused specs also reset the workspace between attempts so repeated runs do not inherit stale leaves from previous scenarios.

Vault folder order: Root folder buttons are sorted by name (ascending). In the generated vault the order is: Archive, File Types Test, Project A, Project B, Reference. Specs that need a project folder with state sections (e.g. search) use folder index 2 (Project A) or the openCardBrowserAndEnterFolderByName helper.

Coverage

Coverage is collected when you run npm run test:unit. Configuration is in jest.config.ts:

  • collectCoverageFrom — Includes src/**/*.{ts,tsx} and excludes *.test.{ts,tsx}, *.d.ts, and src/main.ts so only library code is measured.
  • coverageThreshold — Global minimums (e.g. statements, branches, functions, lines at 20%) to catch regressions. Adjust as the suite grows.
  • Output — V8 provider; report is written to coverage/.

To run tests without coverage: npx jest --coverage=false.

Tested modules

Logic (unit): offset-state, get-file-type-label, get-file-display-name, get-state-and-priority-by-name, folder-processes, section-processes, frontmatter-processes, file-type-filter, file-processes, file-access-processes, is-extension-unsupported, toggle-state-menu, stores.

Utils (unit): string-processes, string-processes-extra, sorting, file-manipulation, misc, storage.

Types (unit): plugin-settings-migrations, migration-helpers, migration tests (0.0.4→0.0.5, 0.0.5→0.1.0, 0.1.0→0.3.0).

Commands (unit): open-project-browser, reveal-in-project-browser (native menu registration, section detection from menu items, section caching by source, null-target skip, alternative Finder titles, no-Finder fallback).

Logic — reveal (unit): reveal-in-project-browser (reveal location for files and folders, multi-select target resolution, empty selection, mixed-parent selection, file without parent folder, existing-leaf reuse, new-leaf creation, concurrent-reveal guard).

Context menus (unit): project-context-menu (priority items, reveal action).

Modals (unit): ConfirmationModal (constructor options).

Components (React): Tooltip, SearchInput, BackButtonAndPath. Others (e.g. card-browser, state-section, modals with heavy Obsidian coupling) are deferred; E2E and manual QA cover integration.

Technical details

  • Jestjest-environment-jsdom for DOM/React support.
  • Babel@babel/preset-react with automatic runtime for JSX.
  • Coverage — V8 provider, output in coverage/.
  • E2E — WebdriverIO 9, Mocha, wdio-obsidian-service. Config: wdio.conf.mts. TypeScript: tsconfig.e2e.json.
  • Startup E2E strategy — Browser startup tests intentionally cover both event-driven entry (active-leaf-change) and layout-driven recovery (layout-change) because empty-leaf transitions do not always come from opening a fresh tab.
  • Reveal menu section detection — The file-menu / files-menu handler inspects the Menu object's internal items array to find the built-in Finder item and read its section. This is not a public Obsidian API; if internal menu structure changes, the reveal item will still appear but may land in a different section. The detected section is cached per menu source string.

Technical gotchas

  • getGlobals() — Components that call getGlobals() require setGlobalsForComponentTest() (or equivalent) before render, or they will throw. Logic tests that use getGlobals() should mock src/logic/stores with jest.mock('./stores', () => ({ getGlobals: jest.fn() })) and set the return value in each test.
  • Obsidian types — Tests use the mocked TFile/TFolder from obsidianMock.js. The mock constructors match the real API (TFile(name, ctime, mtime), TFolder(name)). Add vault, path, or parent on the instance in tests when the code under test expects them.
  • Path aliasessrc/ imports resolve via Jest moduleNameMapper and modulePaths; ensure ^src/main$ is mapped before the generic ^src/(.*)$ pattern.
  • Stores / Jotai — Tests for stores or toggle-state-menu use the real Jotai default store; call setGlobals() in beforeEach so dependent code does not throw. Fake timers are used in toggle-state-menu tests for returnStateMenuAfterDelay.
  • Obsidian Modal mock — The global obsidianMock.js Modal returns a plain object, so subclasses like ConfirmationModal may not have their prototype methods (e.g. onOpen) on the returned instance. Prefer testing constructor-assigned options or mock Modal per test when needed.
  • Closing all tabs is not the same as creating an empty leaf — For startup reliability tests, make sure the final active tab is actually closed last. That produces a different workspace transition than simply calling getLeaf(true) and is the path that exercises “browser opens after all tabs are gone”.