Example Project

examples/demo-addon/ in this repository is a complete, minimal add-on that this kit tests itself against. It’s deliberately boring: one gesture, one spoken phrase, one log line at startup — enough surface to assert against, nothing more.

The add-on

examples/demo-addon/manifest.ini
name = testkit-demo
summary = "Testkit Demo Add-on"
description = """A trivial add-on that exists only so nvda-addon-testkit can test itself."""
author = "Zirek <https://github.com/zirekhq>"
url = https://github.com/zirekhq/nvda-addon-testkit
version = 1.0.0
minimumNVDAVersion = 2024.1
# Kept ahead of any NVDA release for the same reason as spy/manifest.ini: this
# add-on only ever runs inside disposable instances this kit provisions,
# including alpha snapshots whose compat floor isn't known ahead of time.
lastTestedNVDAVersion = 2099.1.0
examples/demo-addon/globalPlugins/testkit_demo.py
"""A deliberately boring add-on: it speaks a known phrase on a known gesture,
and writes a known line to the log at startup. That is enough surface for the
kit's own end-to-end tests to assert against.
"""

from typing import ClassVar

import globalPluginHandler
import ui
from core import postNvdaStartup
from logHandler import log

STARTUP_MESSAGE = "testkit demo add-on loaded"
SPOKEN_PHRASE = "testkit demo says hello"


class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        postNvdaStartup.register(self._logStartup)

    def _logStartup(self):
        log.info(STARTUP_MESSAGE)

    def script_sayHello(self, gesture):
        ui.message(SPOKEN_PHRASE)

    # Read by NVDA's ScriptableObject via mangled-name getattr, not a direct reference.
    __gestures: ClassVar[dict[str, str]] = {"kb:NVDA+shift+control+d": "sayHello"}  # NOSONAR(S4487)

Configure

[tool.nvda-testkit]
addon-bundle = "examples/demo-addon.nvda-addon"

Build the bundle

examples/demo-addon/build.py
#!/usr/bin/env python3
"""Zip the demo add-on into examples/demo-addon.nvda-addon."""

from __future__ import annotations

import zipfile
from pathlib import Path

SOURCE = Path(__file__).resolve().parent
BUNDLE = SOURCE.parent / "demo-addon.nvda-addon"


def build() -> Path:
    files = sorted(
        path
        for path in SOURCE.rglob("*")
        if path.is_file()
        and path.name != "build.py"
        and "__pycache__" not in path.parts
        and path.suffix != ".pyc"
    )
    with zipfile.ZipFile(BUNDLE, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        for path in files:
            archive.write(path, path.relative_to(SOURCE).as_posix())
    print(f"wrote {BUNDLE}")
    return BUNDLE


if __name__ == "__main__":
    build()

Use whatever your own add-on already builds with instead — scons dist, a Makefile target, this same zip-the-folder script. The testkit only cares that a .nvda-addon file lands somewhere the addon-bundle pattern can glob.

Test it

Most tests just ask for nvda and addon_under_test:

def test_it_survives_a_restart(nvda, addon_under_test):
    nvda.restart_harness()
    assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
    before = nvda.speech.index()
    nvda.keys.press("NVDA+shift+control+d")
    nvda.speech.wait_for(SPOKEN_PHRASE, timeout=20, since=before)

A test that owns the install lifecycle itself asks for nvda.addons directly instead — see nvda.addons for what each state means:

@pytest.mark.fresh_nvda
def test_install_is_two_phase_and_completes_on_restart(
    nvda, built_demo_addon, assert_no_unexpected_errors
):
    assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED

    info = nvda.addons.install(built_demo_addon)
    assert info.name == "testkit-demo"
    assert nvda.addons.state("testkit-demo") is AddonState.PENDING_INSTALL

    nvda.restart_harness()
    assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
    assert_no_unexpected_errors(nvda)

    nvda.addons.remove("testkit-demo")
    nvda.restart_harness()
    assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED

Run it

python examples/demo-addon/build.py
pytest tests_e2e/test_demo_addon.py -v

Windows only — see Installation for running the same suite in GitHub Actions.