Loam is free and open source for macOS. Your notes stay plain Markdown files on your disk. Download for macOS.[Download for macOS.]
All pages

Plugins

Testing a plugin

.md

Run a plugin test target with loam plugin test, write logic and UI tests with the harnesses, and check the plugin on a vault.

A generated plugin comes with one test. This page shows how to run the tests with loam plugin test, how to write a logic test and a UI test, how to check the plugin on a vault, and what to avoid.

Run the tests

loam plugin test <id> runs the package tests against the app SDK and prints JSON with ok and the test log. The first run also compiles the test target, so it takes a few minutes.

loam --vault ~/Notes plugin test word-count

A logic test

A logic test builds a kernel on a temporary vault, activates the plugin, runs a command, and checks the result. PluginTestHarness comes from PluginKitTesting. This is the generated test:

Tests/WordCountTests/WordCountPluginTests.swift

import PluginKitTesting
import XCTest
@testable import WordCount

final class WordCountPluginTests: XCTestCase {
    func testSaysHelloWithTheNoteCount() async throws {
        let harness = try PluginTestHarness([WordCountPlugin.self], files: ["one.md": "# One", "two.md": "# Two"])
        let output = try await harness.run("word-count.hello", ["name": "Ada"])
        XCTAssertEqual(output, "Hello Ada. Notes in this vault: 2.")
    }
}

The files argument creates notes in the temporary vault before the test runs. harness.run runs a command by id. End each test with harness.host.deactivateAll().

A UI test

A UI test uses UIPluginHarness from PluginKitUITesting. It gives you the host services, a recording workspace and quick pick, and render(_:), which lays a view out offscreen. This is the generated UI test:

Tests/WordPanelTests/WordPanelPluginTests.swift

import PluginKitUITesting
import XCTest
@testable import WordPanel

@MainActor
final class WordPanelPluginTests: XCTestCase {
    func testAddsItsPanelAndCommand() async throws {
        let harness = try UIPluginHarness(uiPlugins: [WordPanelPlugin.self], files: ["one.md": "# One"])
        defer { harness.close() }
        XCTAssertTrue(harness.report.failed.isEmpty, "\(harness.report.failed)")
        let panel = try XCTUnwrap(harness.panels.all.first { $0.id == "word-panel.panel" })
        _ = harness.render(panel.makeView())
        let output = try await harness.run("word-panel.hello")
        XCTAssertEqual(output, "Hello from Word Panel. Notes in this vault: 1.")
    }
}

Check it on a vault

Build the plugin, turn it on, and run its command on a test vault:

loam --vault ~/Notes plugin build word-count
loam --vault ~/Notes plugin enable word-count
loam --vault ~/Notes word-count.hello

Use a test note for your checks, and delete it afterwards. Keep your own notes unchanged.

Pitfalls

  • In the CLI, each run loads the latest build. An open app keeps the old code until it restarts: loam --vault ~/Notes app relaunch.
  • Read a note right before you write it. The editor may hold text that is not saved yet, and reading through context.vault saves that text first.
  • When ok is false, the last lines of log say why. Fix the first error, then build again.
  • Finish each test with harness.host.deactivateAll(), then check that nothing stays registered.
  • After an app update that changes the SDK major version, rebuild the plugin.
For AI agents: loam guide test has these steps. loam plugin test <id> prints JSON with ok and log.