nvda-addon-testkit 0.1.3__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/PKG-INFO +30 -2
  2. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/README.md +29 -1
  3. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/tests_e2e/conftest.py +6 -0
  4. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/tests_e2e/test_demo_addon.py +5 -5
  5. nvda_addon_testkit-1.1.0/docs/modules/ROOT/examples/tests_e2e/test_demo_dsl.py +63 -0
  6. nvda_addon_testkit-1.1.0/docs/modules/ROOT/examples/tests_e2e/test_dsl_probes.py +29 -0
  7. nvda_addon_testkit-1.1.0/docs/modules/ROOT/examples/tests_e2e/test_modal_dialog_investigation.py +67 -0
  8. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/tests_e2e/test_smoke.py +21 -1
  9. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/nav.adoc +2 -0
  10. nvda_addon_testkit-1.1.0/docs/modules/ROOT/pages/guide/dsl.adoc +154 -0
  11. nvda_addon_testkit-1.1.0/docs/modules/ROOT/pages/guide/modal-dialogs.adoc +48 -0
  12. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/troubleshooting.adoc +1 -1
  13. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/__init__.py +1 -0
  14. nvda_addon_testkit-1.1.0/spy/globalPlugins/nvda_testkit_spy/eval_api.py +81 -0
  15. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/mainthread.py +11 -2
  16. nvda_addon_testkit-1.1.0/spy/globalPlugins/nvda_testkit_spy/modal_api.py +229 -0
  17. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/registry.py +3 -1
  18. nvda_addon_testkit-1.1.0/src/nvda_testkit/_spy/nvda-testkit-spy.nvda-addon +0 -0
  19. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/_version.py +2 -2
  20. nvda_addon_testkit-1.1.0/src/nvda_testkit/actionmark.py +20 -0
  21. nvda_addon_testkit-1.1.0/src/nvda_testkit/client.py +225 -0
  22. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/__init__.py +5 -0
  23. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/dialogs.py +60 -0
  24. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/hearing.py +56 -0
  25. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/lifecycle.py +65 -0
  26. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/logsteps.py +67 -0
  27. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/matching.py +44 -0
  28. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/messages.py +105 -0
  29. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/nvda.py +271 -0
  30. nvda_addon_testkit-1.1.0/src/nvda_testkit/dsl/waiting.py +26 -0
  31. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/errors.py +14 -0
  32. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/addons.py +1 -1
  33. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/keys.py +15 -3
  34. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/plugin.py +15 -4
  35. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/process.py +55 -1
  36. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/rpcclient.py +11 -4
  37. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/settings.py +21 -5
  38. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/conftest.py +40 -0
  39. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/fake_nvda.py +46 -0
  40. nvda_addon_testkit-1.1.0/tests/test_client.py +392 -0
  41. nvda_addon_testkit-1.1.0/tests/test_client_action_mark.py +143 -0
  42. nvda_addon_testkit-1.1.0/tests/test_dsl_dialogs.py +181 -0
  43. nvda_addon_testkit-1.1.0/tests/test_dsl_expecting.py +70 -0
  44. nvda_addon_testkit-1.1.0/tests/test_dsl_hearing.py +111 -0
  45. nvda_addon_testkit-1.1.0/tests/test_dsl_lifecycle.py +126 -0
  46. nvda_addon_testkit-1.1.0/tests/test_dsl_logsteps.py +97 -0
  47. nvda_addon_testkit-1.1.0/tests/test_dsl_messages.py +159 -0
  48. nvda_addon_testkit-1.1.0/tests/test_dsl_nvda.py +88 -0
  49. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_plugin.py +53 -1
  50. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_process.py +113 -0
  51. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_rpcclient.py +23 -1
  52. nvda_addon_testkit-1.1.0/tests/test_settings.py +126 -0
  53. nvda_addon_testkit-1.1.0/tests_spy/test_eval_api.py +129 -0
  54. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_mainthread.py +34 -0
  55. nvda_addon_testkit-1.1.0/tests_spy/test_modal_api.py +203 -0
  56. nvda_addon_testkit-0.1.3/spy/globalPlugins/nvda_testkit_spy/eval_api.py +0 -35
  57. nvda_addon_testkit-0.1.3/src/nvda_testkit/_spy/nvda-testkit-spy.nvda-addon +0 -0
  58. nvda_addon_testkit-0.1.3/src/nvda_testkit/client.py +0 -110
  59. nvda_addon_testkit-0.1.3/tests/test_client.py +0 -123
  60. nvda_addon_testkit-0.1.3/tests/test_settings.py +0 -58
  61. nvda_addon_testkit-0.1.3/tests_spy/test_eval_api.py +0 -47
  62. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/CODEOWNERS +0 -0
  63. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/CONTRIBUTING.md +0 -0
  64. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  65. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  66. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  67. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/pull_request_template.md +0 -0
  68. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/renovate.json +0 -0
  69. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/CI.yml +0 -0
  70. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/notify-docs.yml +0 -0
  71. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/prepare-release.yml +0 -0
  72. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/project-sync.yml +0 -0
  73. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/publish-python.yml +0 -0
  74. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/sonar-fork-coverage.yml +0 -0
  75. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/sonar-fork-scan.yml +0 -0
  76. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/sonar.yml +0 -0
  77. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/stale.yml +0 -0
  78. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.github/workflows/zizmor.yml +0 -0
  79. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.gitignore +0 -0
  80. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/.sonarlint/connectedMode.json +0 -0
  81. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/LICENSE +0 -0
  82. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/action.yml +0 -0
  83. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/antora.yml +0 -0
  84. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/demo-addon/build.py +0 -0
  85. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/demo-addon/globalPlugins/testkit_demo.py +0 -0
  86. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/examples/demo-addon/manifest.ini +0 -0
  87. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/ci-guide.adoc +0 -0
  88. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/contributing.adoc +0 -0
  89. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/example-project.adoc +0 -0
  90. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/addons.adoc +0 -0
  91. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/braille.adoc +0 -0
  92. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/config.adoc +0 -0
  93. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/fixtures.adoc +0 -0
  94. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/keys.adoc +0 -0
  95. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/log.adoc +0 -0
  96. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/guide/speech.adoc +0 -0
  97. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/index.adoc +0 -0
  98. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/installation.adoc +0 -0
  99. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/docs/modules/ROOT/pages/tutorial.adoc +0 -0
  100. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/pyproject.toml +0 -0
  101. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/ruff.toml +0 -0
  102. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/scripts/next-version.sh +0 -0
  103. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/sonar-project.properties +0 -0
  104. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/addons_api.py +0 -0
  105. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/braille_tap.py +0 -0
  106. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/config_api.py +0 -0
  107. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/input_api.py +0 -0
  108. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/log_tap.py +0 -0
  109. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/serialise.py +0 -0
  110. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/server.py +0 -0
  111. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/globalPlugins/nvda_testkit_spy/speech_tap.py +0 -0
  112. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/spy/manifest.ini +0 -0
  113. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/__init__.py +0 -0
  114. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/cli.py +0 -0
  115. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/download.py +0 -0
  116. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/__init__.py +0 -0
  117. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/braille.py +0 -0
  118. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/config.py +0 -0
  119. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/log.py +0 -0
  120. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/namespaces/speech.py +0 -0
  121. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/portable.py +0 -0
  122. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/provisioning.py +0 -0
  123. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/resolve.py +0 -0
  124. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/speechtypes.py +0 -0
  125. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/src/nvda_testkit/spybundle.py +0 -0
  126. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/fixtures/snapshot_index_alpha.html +0 -0
  127. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/fixtures/update_check_stable.txt +0 -0
  128. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_addons_namespace.py +0 -0
  129. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_braille_namespace.py +0 -0
  130. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_build_spy.py +0 -0
  131. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_cli.py +0 -0
  132. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_config_namespace.py +0 -0
  133. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_download.py +0 -0
  134. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_e2e_conftest.py +0 -0
  135. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_errors.py +0 -0
  136. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_fake_nvda.py +0 -0
  137. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_keys_namespace.py +0 -0
  138. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_log_namespace.py +0 -0
  139. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_portable.py +0 -0
  140. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_provisioning.py +0 -0
  141. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_resolve.py +0 -0
  142. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_speech_namespace.py +0 -0
  143. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests/test_speechtypes.py +0 -0
  144. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/__init__.py +0 -0
  145. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/conftest.py +0 -0
  146. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/nvda_stubs.py +0 -0
  147. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_addons_api.py +0 -0
  148. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_braille_tap.py +0 -0
  149. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_config_api.py +0 -0
  150. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_input_api.py +0 -0
  151. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_log_tap.py +0 -0
  152. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_plugin.py +0 -0
  153. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_registry.py +0 -0
  154. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_serialise.py +0 -0
  155. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_server.py +0 -0
  156. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tests_spy/test_speech_tap.py +0 -0
  157. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/tools/build_spy.py +0 -0
  158. {nvda_addon_testkit-0.1.3 → nvda_addon_testkit-1.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nvda-addon-testkit
3
- Version: 0.1.3
3
+ Version: 1.1.0
4
4
  Summary: End-to-end testing for NVDA add-ons against a real NVDA
5
5
  Project-URL: Homepage, https://github.com/ZirekHQ/nvda-addon-testkit
6
6
  Project-URL: Issues, https://github.com/ZirekHQ/nvda-addon-testkit/issues
@@ -44,6 +44,15 @@ def test_my_addon_announces_itself(nvda, addon_under_test):
44
44
  nvda.log.assert_no_errors()
45
45
  ```
46
46
 
47
+ The same test reads as plain steps with the (experimental) DSL:
48
+
49
+ ```python
50
+ def test_my_addon_announces_itself(nvda, addon_under_test):
51
+ nvda.press("NVDA+shift+m")
52
+ nvda.should_hear("my add-on is ready")
53
+ nvda.should_have_no_errors()
54
+ ```
55
+
47
56
  ## Install
48
57
 
49
58
  ```bash
@@ -94,6 +103,25 @@ still on Extended Security Updates).
94
103
  | `nvda.log` | structured log records, and `assert_no_errors()` |
95
104
  | `nvda.addons` | two-phase install, remove, and state |
96
105
 
106
+ `nvda.eval()` runs a single expression inside NVDA; `nvda.exec()` runs a
107
+ full multi-statement scenario and returns whatever it binds to
108
+ `__result__`. Both need `--nvda-allow-eval`. A bad scenario raises
109
+ `ScenarioSyntaxError`, so catching bare `except Exception: pass` around
110
+ either call still swallows it — catch the types you expect instead.
111
+
112
+ `nvda.restart_harness()` kills and relaunches the NVDA process — use it to
113
+ finish a two-phase add-on install or reset to a clean process. It does not
114
+ exercise NVDA's own restart logic. For that, use `nvda.restart_nvda()`,
115
+ which triggers NVDA's real `core.restart()` and waits for the replacement
116
+ process — needs `--nvda-allow-eval`, since it is built on `nvda.eval()`.
117
+
118
+ A real `wx.Dialog.ShowModal()` never returns control to any of the above —
119
+ NVDA's main-thread queue doesn't drain while one is up. Open it with
120
+ `nvda.exec_nowait()` instead of `exec()` (queues the scenario without
121
+ waiting for it to finish), then close it with `nvda.simulate_modal(gesture,
122
+ timeout=10.0)`, which sends real injected keyboard input once our process
123
+ takes the foreground.
124
+
97
125
  ## Requirements
98
126
 
99
127
  Windows to run the tests. NVDA is downloaded automatically — you do not need
@@ -126,4 +154,4 @@ If this repository saves you time and effort, please consider supporting it!
126
154
 
127
155
  - ⭐ [Star on GitHub](https://github.com/ZirekHQ/nvda-addon-testkit)
128
156
  - 🐦 [Share on Twitter](https://twitter.com/intent/tweet?text=nvda-addon-testkit%20-%20real%20end-to-end%20testing%20for%20NVDA%20add-ons&url=https%3A%2F%2Fgithub.com%2FZirekHQ%2Fnvda-addon-testkit)
129
- - 💖 [More ways to support](https://github.com/ZirekHQ) — Open Collective coming soon
157
+ - 💖 [Support on Open Collective](https://opencollective.com/zirek)
@@ -17,6 +17,15 @@ def test_my_addon_announces_itself(nvda, addon_under_test):
17
17
  nvda.log.assert_no_errors()
18
18
  ```
19
19
 
20
+ The same test reads as plain steps with the (experimental) DSL:
21
+
22
+ ```python
23
+ def test_my_addon_announces_itself(nvda, addon_under_test):
24
+ nvda.press("NVDA+shift+m")
25
+ nvda.should_hear("my add-on is ready")
26
+ nvda.should_have_no_errors()
27
+ ```
28
+
20
29
  ## Install
21
30
 
22
31
  ```bash
@@ -67,6 +76,25 @@ still on Extended Security Updates).
67
76
  | `nvda.log` | structured log records, and `assert_no_errors()` |
68
77
  | `nvda.addons` | two-phase install, remove, and state |
69
78
 
79
+ `nvda.eval()` runs a single expression inside NVDA; `nvda.exec()` runs a
80
+ full multi-statement scenario and returns whatever it binds to
81
+ `__result__`. Both need `--nvda-allow-eval`. A bad scenario raises
82
+ `ScenarioSyntaxError`, so catching bare `except Exception: pass` around
83
+ either call still swallows it — catch the types you expect instead.
84
+
85
+ `nvda.restart_harness()` kills and relaunches the NVDA process — use it to
86
+ finish a two-phase add-on install or reset to a clean process. It does not
87
+ exercise NVDA's own restart logic. For that, use `nvda.restart_nvda()`,
88
+ which triggers NVDA's real `core.restart()` and waits for the replacement
89
+ process — needs `--nvda-allow-eval`, since it is built on `nvda.eval()`.
90
+
91
+ A real `wx.Dialog.ShowModal()` never returns control to any of the above —
92
+ NVDA's main-thread queue doesn't drain while one is up. Open it with
93
+ `nvda.exec_nowait()` instead of `exec()` (queues the scenario without
94
+ waiting for it to finish), then close it with `nvda.simulate_modal(gesture,
95
+ timeout=10.0)`, which sends real injected keyboard input once our process
96
+ takes the foreground.
97
+
70
98
  ## Requirements
71
99
 
72
100
  Windows to run the tests. NVDA is downloaded automatically — you do not need
@@ -99,4 +127,4 @@ If this repository saves you time and effort, please consider supporting it!
99
127
 
100
128
  - ⭐ [Star on GitHub](https://github.com/ZirekHQ/nvda-addon-testkit)
101
129
  - 🐦 [Share on Twitter](https://twitter.com/intent/tweet?text=nvda-addon-testkit%20-%20real%20end-to-end%20testing%20for%20NVDA%20add-ons&url=https%3A%2F%2Fgithub.com%2FZirekHQ%2Fnvda-addon-testkit)
102
- - 💖 [More ways to support](https://github.com/ZirekHQ) — Open Collective coming soon
130
+ - 💖 [Support on Open Collective](https://opencollective.com/zirek)
@@ -65,3 +65,9 @@ def check_no_unexpected_errors(client, *, since: int = 0) -> None:
65
65
  @pytest.fixture
66
66
  def assert_no_unexpected_errors():
67
67
  return check_no_unexpected_errors
68
+
69
+
70
+ @pytest.fixture
71
+ def require_eval(pytestconfig):
72
+ if not pytestconfig.option.nvda_allow_eval:
73
+ pytest.skip("needs --nvda-allow-eval")
@@ -27,19 +27,19 @@ def test_install_is_two_phase_and_completes_on_restart(
27
27
  assert info.name == "testkit-demo"
28
28
  assert nvda.addons.state("testkit-demo") is AddonState.PENDING_INSTALL
29
29
 
30
- nvda.restart()
30
+ nvda.restart_harness()
31
31
  assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
32
32
  assert_no_unexpected_errors(nvda)
33
33
 
34
34
  nvda.addons.remove("testkit-demo")
35
- nvda.restart()
35
+ nvda.restart_harness()
36
36
  assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED
37
37
  # end::addons[]
38
38
 
39
39
 
40
40
  def test_the_installed_addon_logs_at_startup(nvda, addon_under_test):
41
41
  # tag::log[]
42
- nvda.restart()
42
+ nvda.restart_harness()
43
43
  nvda.log.wait_for(re.escape(STARTUP_MESSAGE), since=0, timeout=20)
44
44
  # end::log[]
45
45
 
@@ -53,7 +53,7 @@ def test_its_gesture_produces_the_expected_speech(nvda, addon_under_test):
53
53
 
54
54
  def test_it_survives_a_restart(nvda, addon_under_test):
55
55
  # tag::fixtures[]
56
- nvda.restart()
56
+ nvda.restart_harness()
57
57
  assert nvda.addons.state("testkit-demo") is AddonState.ENABLED
58
58
  before = nvda.speech.index()
59
59
  nvda.keys.press("NVDA+shift+control+d")
@@ -66,5 +66,5 @@ def test_removal_is_also_two_phase(nvda, addon_under_test):
66
66
  and that session-scoped fixture will not reinstall it."""
67
67
  nvda.addons.remove("testkit-demo")
68
68
  assert nvda.addons.state("testkit-demo") is AddonState.PENDING_REMOVE
69
- nvda.restart()
69
+ nvda.restart_harness()
70
70
  assert nvda.addons.state("testkit-demo") is AddonState.NOT_INSTALLED
@@ -0,0 +1,63 @@
1
+ """The demo add-on driven through the DSL. Each tagged block is a docs example.
2
+
3
+ This file assumes the demo add-on is not installed when it starts: test_demo_addon.py,
4
+ sorted earlier, uninstalls it last.
5
+ """
6
+
7
+ import pytest
8
+
9
+ # Mirrors RUNNER_ENVIRONMENT_ERRORS in tests_e2e/conftest.py.
10
+ RUNNER_NOISE = (
11
+ r"nvwave|WASAPI|audio (?:device|output|session|endpoint)",
12
+ r"synthDriver|synthesi[sz]|espeak|oneCore|SAPI",
13
+ r"braille ?display|brailleDisplayDriver|brailleInput",
14
+ r"UIAHandler|IAccessible|interactive desktop|desktop object",
15
+ )
16
+
17
+
18
+ @pytest.mark.fresh_nvda
19
+ def test_install_and_remove_are_one_step_each(nvda):
20
+ # tag::dsl-lifecycle[]
21
+ nvda.should_have_addon("testkit-demo", "not installed")
22
+ nvda.install_addon()
23
+ nvda.should_have_addon("testkit-demo", "enabled")
24
+ nvda.remove_addon("testkit-demo")
25
+ nvda.should_have_addon("testkit-demo", "not installed")
26
+ # end::dsl-lifecycle[]
27
+
28
+
29
+ def test_the_gesture_announces_the_phrase(nvda):
30
+ nvda.install_addon()
31
+ # tag::dsl-basic[]
32
+ nvda.press("NVDA+shift+control+d")
33
+ nvda.should_hear("testkit demo says hello")
34
+ # end::dsl-basic[]
35
+
36
+
37
+ def test_startup_logs_the_loaded_message(nvda):
38
+ nvda.install_addon()
39
+ nvda.relaunch()
40
+ nvda.should_log("testkit demo add-on loaded", within=20)
41
+ nvda.should_have_no_errors(ignoring=list(RUNNER_NOISE))
42
+
43
+
44
+ def test_the_block_form_waits_for_the_speech_a_step_causes(nvda):
45
+ nvda.install_addon()
46
+ # tag::dsl-expecting[]
47
+ with nvda.expecting_speech("testkit demo says hello"):
48
+ nvda.press("NVDA+shift+control+d")
49
+ # end::dsl-expecting[]
50
+
51
+
52
+ def test_a_real_dialog_opens_and_closes_in_a_block(require_eval, nvda):
53
+ # tag::dsl-dialog[]
54
+ with nvda.dialog(
55
+ "import wx\n"
56
+ "dlg = wx.MessageDialog(None, 'confirm?', 'confirm?', wx.YES_NO)\n"
57
+ "dlg.ShowModal()\n"
58
+ "dlg.Destroy()\n",
59
+ close_with="enter",
60
+ ):
61
+ pass
62
+ # end::dsl-dialog[]
63
+ nvda.wait_until_idle(timeout=15)
@@ -0,0 +1,29 @@
1
+ """Probes for DSL behaviour not yet verified on real NVDA."""
2
+
3
+ import pytest
4
+
5
+ DIALOG = (
6
+ "import wx\n"
7
+ "dlg = wx.MessageDialog(None, 'confirm?', 'confirm?', wx.YES_NO)\n"
8
+ "dlg.ShowModal()\n"
9
+ "dlg.Destroy()\n"
10
+ )
11
+
12
+
13
+ def test_speech_and_log_reads_work_while_a_modal_is_open(require_eval, nvda):
14
+ nvda.open_dialog(DIALOG)
15
+ try:
16
+ assert isinstance(nvda.speech.index(), int)
17
+ assert isinstance(nvda.log.all(), list)
18
+ finally:
19
+ nvda.close_dialog("enter")
20
+
21
+
22
+ @pytest.mark.parametrize("text", ["Hello", "a.b,c", "x!y"])
23
+ def test_typing_characters_beyond_lowercase_letters(nvda, text):
24
+ nvda.type(text)
25
+
26
+
27
+ @pytest.mark.xfail(strict=False, reason="non-ASCII gesture names are unverified")
28
+ def test_typing_non_ascii_characters(nvda):
29
+ nvda.type("ünï")
@@ -0,0 +1,67 @@
1
+ """Investigation for issue #34, gaps 2 & 5: does a queued main-thread job
2
+ start while a real ShowModal() dialog is up?
3
+
4
+ This is not a regression test. It is a one-shot probe: run it once on
5
+ Windows CI, read the result, and act on it per the plan (either open a new
6
+ issue describing a real fix, or proceed with the simulate_modal() fallback
7
+ in the next task). Delete or keep this file once the investigation is
8
+ resolved -- it is not meant to run on every CI build.
9
+
10
+ Everything happens inside a single exec_in_nvda call, entirely on NVDA's
11
+ main thread: the spy's XML-RPC server (server.py) is a plain, unthreaded
12
+ SimpleXMLRPCServer, so two separate RPC connections can never be genuinely
13
+ concurrent at the transport level -- the second call simply can't be
14
+ dispatched until the first's handler returns. Queuing the second job from
15
+ *inside* the running scenario, via queueHandler.queueFunction directly,
16
+ avoids needing RPC-level concurrency at all: it tests whether NVDA's own
17
+ queue-draining mechanism still runs while the main thread is nested inside
18
+ ShowModal()'s event loop, using a timestamp comparison instead of a second
19
+ network round trip.
20
+
21
+ A third, lower-probability outcome is possible: if the dialog's own
22
+ wx.CallLater dismiss timer never fires for some unrelated reason, this
23
+ scenario never returns from ShowModal(), so nvda.exec() raises an RpcError
24
+ (wrapping exec_in_nvda's server-side "started on NVDA's main thread but did
25
+ not return within 30.0s" timeout) instead of returning a __result__ to
26
+ assert on. If you see that on a real run, it's worth investigating
27
+ separately -- it doesn't confirm or refute the queue-blocking hypothesis
28
+ either way.
29
+ """
30
+
31
+
32
+ def test_a_second_job_while_a_real_modal_is_up(require_eval, nvda):
33
+ scenario = (
34
+ "import queueHandler\n"
35
+ "import time\n"
36
+ "import wx\n"
37
+ "job_b_ran_at = []\n"
38
+ # exec_in_nvda runs this with separate globals/locals dicts (like a
39
+ # class body), so a nested def can't see job_b_ran_at/time as
40
+ # globals -- bind both as defaults, evaluated now, in this scope.
41
+ "def job_b(sink=job_b_ran_at, now=time.monotonic):\n"
42
+ " sink.append(now())\n"
43
+ "queueHandler.queueFunction(queueHandler.eventQueue, job_b)\n"
44
+ "dlg = wx.MessageDialog(None, 'probe', 'probe', wx.YES_NO)\n"
45
+ "wx.CallLater(1000, dlg.EndModal, wx.ID_YES)\n"
46
+ "before = time.monotonic()\n"
47
+ "dlg.ShowModal()\n"
48
+ "after = time.monotonic()\n"
49
+ "dlg.Destroy()\n"
50
+ "__result__ = {\n"
51
+ " 'ran_during_modal': bool(job_b_ran_at) and before <= job_b_ran_at[0] <= after,\n"
52
+ " 'ran_at_all': bool(job_b_ran_at),\n"
53
+ "}\n"
54
+ )
55
+
56
+ result = nvda.exec(scenario)
57
+
58
+ assert result["ran_during_modal"], (
59
+ "CONFIRMS THE DEADLOCK: a second main-thread job never ran while a "
60
+ f"real ShowModal() dialog was up (ran_at_all={result['ran_at_all']!r}). "
61
+ "Proceed with Task 7 (the simulate_modal() fallback)."
62
+ )
63
+ # If this assertion passes instead, the queue IS drained during
64
+ # ShowModal() in this NVDA version: STOP, do not proceed to Task 7, and
65
+ # open a new issue describing this finding plus what actually blocks
66
+ # input_api.py's keys_press() from dismissing the dialog (if anything
67
+ # still does).
@@ -37,5 +37,25 @@ def test_config_round_trips_through_a_real_nvda(nvda):
37
37
 
38
38
 
39
39
  def test_startup_produced_no_errors(nvda, assert_no_unexpected_errors):
40
- nvda.restart()
40
+ nvda.restart_harness()
41
41
  assert_no_unexpected_errors(nvda)
42
+
43
+
44
+ def test_restart_nvda_exercises_core_restart(require_eval, nvda, assert_no_unexpected_errors):
45
+ old_pid = nvda.process.handshake.pid
46
+ nvda.restart_nvda(timeout=60)
47
+ assert nvda.process.handshake.pid != old_pid
48
+ assert_no_unexpected_errors(nvda)
49
+
50
+
51
+ def test_simulate_modal_closes_a_real_dialog(require_eval, nvda):
52
+ # tag::modal[]
53
+ nvda.exec_nowait(
54
+ "import wx\n"
55
+ "dlg = wx.MessageDialog(None, 'confirm?', 'confirm?', wx.YES_NO)\n"
56
+ "dlg.ShowModal()\n"
57
+ "dlg.Destroy()\n"
58
+ )
59
+ assert nvda.simulate_modal("enter", timeout=10)
60
+ # end::modal[]
61
+ nvda.wait_until_idle(timeout=15)
@@ -3,12 +3,14 @@
3
3
  * xref:tutorial.adoc[Writing Your First Test]
4
4
  * Guide
5
5
  ** xref:guide/fixtures.adoc[Fixtures]
6
+ ** xref:guide/dsl.adoc[Writing tests with the DSL]
6
7
  ** xref:guide/speech.adoc[speech]
7
8
  ** xref:guide/braille.adoc[braille]
8
9
  ** xref:guide/keys.adoc[keys]
9
10
  ** xref:guide/config.adoc[config]
10
11
  ** xref:guide/log.adoc[log]
11
12
  ** xref:guide/addons.adoc[addons]
13
+ ** xref:guide/modal-dialogs.adoc[Closing a real modal dialog]
12
14
  * xref:example-project.adoc[Example Project]
13
15
  * xref:ci-guide.adoc[Configuring CI]
14
16
  * xref:troubleshooting.adoc[Troubleshooting]
@@ -0,0 +1,154 @@
1
+ = Writing tests with the DSL
2
+
3
+ NOTE: The DSL is experimental. Names may change in minor releases until it is marked stable.
4
+
5
+ The `nvda` fixture reads as a list of plain steps. Each step is one sentence on one line. Nothing
6
+ needs nesting, and failure messages are plain numbered text with no colour or tables, so they read
7
+ cleanly through a screen reader.
8
+
9
+ The examples assume the add-on under test is installed. Call `nvda.install_addon()` in the test, or
10
+ request the `addon_under_test` fixture.
11
+
12
+ [source,python]
13
+ ----
14
+ def test_the_gesture_announces_the_phrase(nvda):
15
+ include::example$tests_e2e/test_demo_dsl.py[tag=dsl-basic,indent=4]
16
+ ----
17
+
18
+ == Actions
19
+
20
+ Actions send input or change NVDA's state. Each one records where speech begins to count for the
21
+ next assertion, so you never pass `since=`.
22
+
23
+ * `nvda.press("NVDA+t")` sends a gesture.
24
+ * `nvda.type("text")` sends one gesture per character. A character that is not a valid gesture
25
+ name raises an error that names it.
26
+ * `nvda.relaunch()` kills and relaunches the NVDA process. The add-on stays installed.
27
+ * `nvda.restart_nvda()` runs NVDA's own restart. Needs `--nvda-allow-eval`.
28
+
29
+ == Assertions
30
+
31
+ * `nvda.should_hear("text")` waits for speech produced after the last action.
32
+ * `nvda.should_hear(matching=r"\d+:\d+")` does the same with a regular expression.
33
+ * `nvda.should_not_hear("error", for_seconds=1)` watches for a fixed time. Put a `should_hear`
34
+ before it, and raise `for_seconds` on slow machines.
35
+ * `nvda.should_log("text")` waits for a log record. It searches the log since the test began, or
36
+ since the last relaunch, because a relaunch starts a new NVDA process with a fresh log. It does
37
+ not restrict the search to records written after the last action.
38
+ * `nvda.should_have_no_errors()` fails on logged errors. Pass `ignoring=[...]` or set
39
+ `ignore-log-errors` to skip runner noise.
40
+ * `nvda.should_have_addon("name", "enabled")` checks an add-on's state.
41
+
42
+ Text matching is plain and case-insensitive, and finds the text anywhere in an utterance. Use
43
+ `matching=` only when you need a regex. A string pattern is searched case-insensitively; a compiled
44
+ pattern keeps its own flags. The wait defaults to 10 seconds. Change it per call with `within=20`,
45
+ or for the project with `timeout = 20` under `[tool.nvda-testkit]`.
46
+
47
+ Two `should_hear` calls after one action each search all of that action's speech. They do not
48
+ check order, and two identical calls pass on a single occurrence.
49
+
50
+ == Waiting for speech around a block
51
+
52
+ [source,python]
53
+ ----
54
+ def test_it_speaks_after_the_step(nvda):
55
+ include::example$tests_e2e/test_demo_dsl.py[tag=dsl-expecting,indent=4]
56
+ ----
57
+
58
+ If the block relaunches NVDA, the search covers everything the new process said.
59
+
60
+ == Add-on lifecycle
61
+
62
+ Mark the test with `@pytest.mark.fresh_nvda` so it starts from a clean add-on state. The included
63
+ snippet omits the decorator.
64
+
65
+ [source,python]
66
+ ----
67
+ def test_install_and_remove(nvda):
68
+ include::example$tests_e2e/test_demo_dsl.py[tag=dsl-lifecycle,indent=4]
69
+ ----
70
+
71
+ `install_addon()` installs the bundle from the `addon-bundle` setting, or the path you pass, then
72
+ relaunches NVDA and checks that the add-on is enabled. `remove_addon(name)` removes it, relaunches,
73
+ and checks that it is gone. An add-on installed with `install_addon()` is removed again when the
74
+ test ends.
75
+
76
+ == Dialogs
77
+
78
+ [source,python]
79
+ ----
80
+ def test_a_dialog(nvda):
81
+ include::example$tests_e2e/test_demo_dsl.py[tag=dsl-dialog,indent=4]
82
+ ----
83
+
84
+ `open_dialog()` and `close_dialog()` are the same steps without the block. While a dialog is open,
85
+ the steps that send input or change state raise an error instead of hanging: `press`, `type`,
86
+ `relaunch`, `restart_nvda`, `restart_harness`, `install_addon`, `remove_addon`, `should_have_addon`,
87
+ `expecting_speech` and `open_dialog`. The assertions that only read speech and log (`should_hear`,
88
+ `should_not_hear`, `should_log` and `should_have_no_errors`) are not guarded and work while a dialog
89
+ is open. The low-level passthroughs `exec`, `eval`, `exec_nowait`, `simulate_modal`,
90
+ `wait_until_idle`, `reset` and `keys.*` are not guarded and still block. Needs
91
+ `--nvda-allow-eval`.
92
+
93
+ A test that ends with a dialog open fails after the kit closes it.
94
+
95
+ == Teardown
96
+
97
+ When a test ends, the `nvda` fixture runs three checks in order. With `fail-on-log-errors`, it
98
+ checks the log first. It then closes a leaked dialog. It then removes the add-ons installed with
99
+ `install_addon()`. Every check runs even if an earlier one fails, and the problems are reported
100
+ together as one teardown error.
101
+
102
+ == Reading failures
103
+
104
+ A failed `should_hear` states what it expected, the action before it, the time waited, and what was
105
+ heard, numbered, one item per line:
106
+
107
+ ----
108
+ Expected to hear "PM" within 10 seconds after pressing NVDA+t.
109
+ Time elapsed: 10.05 seconds.
110
+ Heard since that action, 2 items:
111
+ 1. "12 colon 00"
112
+ 2. "Tuesday"
113
+ Nothing matched. Matching is case-insensitive plain text; use matching= for a regex.
114
+ ----
115
+
116
+ Long lists in `should_hear` and `should_log` messages show ten items. `should_have_no_errors` shows
117
+ six unexpected and three ignored records. Run with `--nvda-verbose` to see all of them. For
118
+ terminal output without colour, run `pytest --color=no`.
119
+
120
+ == Before and after
121
+
122
+ Without the DSL:
123
+
124
+ [source,python]
125
+ ----
126
+ before = nvda.speech.index()
127
+ nvda.keys.press("NVDA+t")
128
+ found = nvda.speech.wait_for("12:00", timeout=10, since=before)
129
+ assert "12:00" in found.text
130
+ ----
131
+
132
+ With it:
133
+
134
+ [source,python]
135
+ ----
136
+ nvda.press("NVDA+t")
137
+ nvda.should_hear("12:00")
138
+ ----
139
+
140
+ The old API keeps working, and `nvda.speech`, `nvda.keys` and the other namespaces are still there.
141
+
142
+ == Project settings
143
+
144
+ [source,toml]
145
+ ----
146
+ [tool.nvda-testkit]
147
+ timeout = 20
148
+ fail-on-log-errors = true
149
+ ignore-log-errors = ["nvwave", "WASAPI"]
150
+ ----
151
+
152
+ With `fail-on-log-errors`, every test that uses `nvda` reports a teardown error if NVDA logged an
153
+ unignored error. Each `ignore-log-errors` entry is a regular expression, searched
154
+ case-insensitively.
@@ -0,0 +1,48 @@
1
+ = Closing a real modal dialog
2
+
3
+ `nvda.exec()` and `nvda.eval()` both dispatch through NVDA's own main-thread
4
+ queue and wait for the scenario to finish. That works for anything that
5
+ returns on its own, but a real `wx.Dialog.ShowModal()` never drains that
6
+ queue for as long as it's up — confirmed against a real NVDA, not just
7
+ suspected. A scenario that opens one and waits for `exec()` to return will
8
+ sit there until `exec()`'s own timeout fires, with the dialog still open
9
+ afterwards. `nvda.keys.press()` can't reach it either, for the same reason:
10
+ it dispatches through the same queue.
11
+
12
+ The dialog's own message loop is still alive, though — it has to be, to
13
+ receive the click a real user would make. `exec_nowait()` and
14
+ `simulate_modal()` reach it that way instead.
15
+
16
+ [source,python]
17
+ ----
18
+ def test_simulate_modal_closes_a_real_dialog(nvda):
19
+ include::example$tests_e2e/test_smoke.py[tag=modal,indent=4]
20
+ ----
21
+
22
+ `exec_nowait()` queues a scenario the same way `exec()` does, but returns
23
+ immediately instead of waiting for it to finish — freeing this process's
24
+ single-threaded RPC server to accept the next call while the scenario is
25
+ stuck inside `ShowModal()`. A syntax error in the scenario still raises
26
+ `ScenarioSyntaxError` synchronously, the same as `exec()`; only running the
27
+ scenario is deferred.
28
+
29
+ `simulate_modal(gesture, timeout=10.0)` then runs on that next call's own
30
+ thread, never touching the blocked queue: it polls for our own process to
31
+ take the foreground — what a modal dialog does unconditionally on showing —
32
+ and, once it does, sends `gesture` as real injected keyboard input via
33
+ Win32's `SendInput`, the same path a human's keypress takes. It returns
34
+ `False` on a timeout instead of raising, since that usually means the
35
+ scenario never actually opened a dialog rather than NVDA hanging.
36
+ `gesture` is one of `"enter"`, `"escape"`, `"tab"`, `"space"`, `"yes"`, or
37
+ `"no"`.
38
+
39
+ `exec_nowait()` needs `--nvda-allow-eval`, the same as `eval()`/`exec()`,
40
+ because it runs arbitrary code inside NVDA. `simulate_modal()` does not need
41
+ this flag because it only injects keyboard input.
42
+
43
+ Pair the two: `exec_nowait()` to open the dialog, `simulate_modal()` to
44
+ close it. `exec_nowait()` records which window is in the foreground before it
45
+ queues the scenario, and the next `simulate_modal()` call treats only a
46
+ different window as the dialog. Calling `exec()` instead of `exec_nowait()`
47
+ to open the dialog defeats the point — `simulate_modal()`'s call would never
48
+ even be dispatched.
@@ -63,7 +63,7 @@ the current one; clean it out or narrow the pattern.
63
63
  fails.
64
64
 
65
65
  Install is two-phase by design — `install()` only reaches
66
- `PENDING_INSTALL`; a call to `nvda.restart()` is what completes it. See
66
+ `PENDING_INSTALL`; a call to `nvda.restart_harness()` is what completes it. See
67
67
  xref:guide/addons.adoc[].
68
68
 
69
69
  == Speech assertion mismatches
@@ -17,6 +17,7 @@ from . import braille_tap, log_tap, speech_tap
17
17
  from . import config_api as config_api
18
18
  from . import eval_api as eval_api
19
19
  from . import input_api as input_api
20
+ from . import modal_api as modal_api
20
21
  from .server import SpyServer
21
22
 
22
23
 
@@ -0,0 +1,81 @@
1
+ # coding: utf-8
2
+ """Run code inside NVDA's own process: one expression, or a scenario.
3
+
4
+ The host refuses to call either unless the session opted in, so the spy does
5
+ not second-guess it: the point is to reach NVDA's live state, which means
6
+ full builtins and real imports. Both run on the main thread for the same
7
+ reason every other mutation does.
8
+
9
+ eval_in_nvda evaluates a single expression and returns its value.
10
+ exec_in_nvda runs one or more statements and returns whatever the code bound
11
+ to a name called __result__, or None if it bound nothing -- multi-statement
12
+ scenarios (e.g. "import core; core.restart()") do not compile under eval()
13
+ and previously needed an unreadable immediately-invoked-lambda workaround.
14
+
15
+ exec_in_nvda_nowait queues a scenario onto the main thread like exec_in_nvda
16
+ does, but does not wait for it to finish before returning. Use it for a
17
+ scenario that opens a real modal dialog: exec_in_nvda would block this
18
+ process's single-threaded RPC server for the dialog's whole lifetime, so a
19
+ paired simulate_modal call (see modal_api.py) could never even be
20
+ dispatched to close it.
21
+ """
22
+
23
+ import builtins
24
+
25
+ import queueHandler
26
+ from logHandler import log
27
+
28
+ from .mainthread import run_on_main_thread
29
+ from .modal_api import remember_foreground_baseline
30
+ from .registry import rpc_method
31
+
32
+ _SCALARS = (str, int, float, bool, type(None))
33
+
34
+
35
+ def _marshallable(value):
36
+ """xmlrpc carries scalars and containers of scalars; everything else is a repr."""
37
+ if isinstance(value, _SCALARS):
38
+ return value
39
+ if isinstance(value, dict):
40
+ return {str(key): _marshallable(item) for key, item in value.items()}
41
+ if isinstance(value, (list, tuple, set, frozenset)):
42
+ return [_marshallable(item) for item in value]
43
+ return repr(value)
44
+
45
+
46
+ def _evaluate(source):
47
+ return eval(source, {"__builtins__": builtins})
48
+
49
+
50
+ @rpc_method
51
+ def eval_in_nvda(source, timeout=30.0):
52
+ return _marshallable(run_on_main_thread(lambda: _evaluate(source), timeout=timeout))
53
+
54
+
55
+ def _execute(source):
56
+ scope = {"__builtins__": builtins}
57
+ exec(compile(source, "<nvda-testkit>", "exec"), scope)
58
+ return scope.get("__result__")
59
+
60
+
61
+ @rpc_method
62
+ def exec_in_nvda(source, timeout=30.0):
63
+ return _marshallable(run_on_main_thread(lambda: _execute(source), timeout=timeout))
64
+
65
+
66
+ @rpc_method
67
+ def exec_in_nvda_nowait(source):
68
+ # Compiled here, synchronously, so a SyntaxError still surfaces on this
69
+ # call the same way exec_in_nvda's does -- only *running* the scenario
70
+ # (which may never return, if it opens a modal dialog) gets queued.
71
+ code = compile(source, "<nvda-testkit>", "exec")
72
+
73
+ def _run():
74
+ try:
75
+ exec(code, {"__builtins__": builtins})
76
+ except Exception:
77
+ log.error("nvda-testkit: exec_in_nvda_nowait scenario raised", exc_info=True)
78
+
79
+ remember_foreground_baseline()
80
+ queueHandler.queueFunction(queueHandler.eventQueue, _run)
81
+ return True