@testspectra/cli 1.1.7 → 1.1.8-rc.10

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 (256) hide show
  1. package/README.md +47 -0
  2. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  3. package/demo-app/assets/index-DOtRypCa.js +154 -0
  4. package/demo-app/index.html +2 -2
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/commands/__tests__/run-e2e.test.js +254 -13
  7. package/dist/commands/__tests__/run.test.js +92 -16
  8. package/dist/commands/add.js +2 -1
  9. package/dist/commands/demo.js +17 -7
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.js +8 -2
  12. package/dist/commands/lint.d.ts +5 -0
  13. package/dist/commands/lint.js +55 -0
  14. package/dist/commands/refactor.d.ts +2 -1
  15. package/dist/commands/refactor.js +129 -35
  16. package/dist/commands/run.d.ts +2 -3
  17. package/dist/commands/run.js +93 -28
  18. package/dist/commands/skills.d.ts +9 -0
  19. package/dist/commands/skills.js +28 -0
  20. package/dist/commands/sync-types.js +1 -0
  21. package/dist/commands/test.d.ts +17 -0
  22. package/dist/commands/test.js +104 -0
  23. package/dist/commands/watch.js +9 -3
  24. package/dist/config/loader.js +1 -1
  25. package/dist/config/schema.d.ts +5 -114
  26. package/dist/config/schema.js +1 -17
  27. package/dist/generator/__tests__/tsconfig-isolation.test.js +6 -2
  28. package/dist/generator/__tests__/type-generator.test.js +39 -1
  29. package/dist/generator/tsconfig-generator.js +44 -16
  30. package/dist/generator/type-generator.d.ts +29 -3
  31. package/dist/generator/type-generator.js +115 -22
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +65 -29
  34. package/dist/linter/__tests__/anonymous-export.test.js +184 -0
  35. package/dist/linter/__tests__/component-test-imports.test.d.ts +1 -0
  36. package/dist/linter/__tests__/component-test-imports.test.js +115 -0
  37. package/dist/linter/__tests__/shared-lib-boundary.test.d.ts +1 -0
  38. package/dist/linter/__tests__/shared-lib-boundary.test.js +270 -0
  39. package/dist/linter/__tests__/spec-schema.test.d.ts +1 -0
  40. package/dist/linter/__tests__/spec-schema.test.js +369 -0
  41. package/dist/linter/__tests__/step-parity.test.d.ts +1 -0
  42. package/dist/linter/__tests__/step-parity.test.js +372 -0
  43. package/dist/linter/discovery.d.ts +30 -0
  44. package/dist/linter/discovery.js +249 -0
  45. package/dist/linter/extractor.d.ts +67 -0
  46. package/dist/linter/extractor.js +245 -0
  47. package/dist/linter/index.d.ts +7 -0
  48. package/dist/linter/index.js +7 -0
  49. package/dist/linter/parser.d.ts +24 -0
  50. package/dist/linter/parser.js +219 -0
  51. package/dist/linter/schemas/spec.schema.d.ts +83 -0
  52. package/dist/linter/schemas/spec.schema.js +150 -0
  53. package/dist/linter/schemas/suite.schema.d.ts +59 -0
  54. package/dist/linter/schemas/suite.schema.js +83 -0
  55. package/dist/linter/spec-linter.d.ts +23 -0
  56. package/dist/linter/spec-linter.js +630 -0
  57. package/dist/linter/types.d.ts +28 -0
  58. package/dist/linter/types.js +1 -0
  59. package/dist/refactor/__tests__/refactor-engine.test.js +698 -0
  60. package/dist/refactor/refactor-engine.d.ts +25 -0
  61. package/dist/refactor/refactor-engine.js +374 -42
  62. package/dist/reporter/semantic-formatter.js +22 -2
  63. package/dist/reporter/types.d.ts +1 -1
  64. package/dist/runner/bridge.d.ts +1 -1
  65. package/dist/runner/bridge.js +85 -24
  66. package/dist/runner/reporter.d.ts +2 -0
  67. package/dist/runner/reporter.js +11 -3
  68. package/dist/utils/__tests__/api-server.test.d.ts +1 -0
  69. package/dist/utils/__tests__/api-server.test.js +62 -0
  70. package/dist/utils/__tests__/demo-server.test.d.ts +1 -0
  71. package/dist/utils/__tests__/demo-server.test.js +35 -0
  72. package/dist/utils/api-server.d.ts +19 -0
  73. package/dist/utils/api-server.js +179 -0
  74. package/dist/utils/demo-server.d.ts +5 -0
  75. package/dist/utils/demo-server.js +8 -1
  76. package/package.json +13 -4
  77. package/templates/default/CLAUDE.md +5 -0
  78. package/templates/default/package.json +3 -3
  79. package/templates/default/page-objects/AuthPage/mobile.ts +5 -0
  80. package/templates/default/page-objects/AuthPage/web.ts +10 -0
  81. package/templates/default/page-objects/MatchersPage/mobile.ts +26 -2
  82. package/templates/default/page-objects/MatchersPage/web.ts +8 -2
  83. package/templates/default/page-objects/NavigationPage/mobile.ts +1 -1
  84. package/templates/default/page-objects/NavigationPage/web.ts +1 -1
  85. package/templates/default/page-objects/PlaygroundPage/mobile.ts +12 -6
  86. package/templates/default/page-objects/PlaygroundPage/web.ts +2 -2
  87. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +45 -3
  88. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/spec.md +7 -0
  89. package/templates/default/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +8 -4
  90. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/android.test.ts +3 -0
  91. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/common.test.ts +3 -0
  92. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +13 -2
  93. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/spec.md +7 -0
  94. package/templates/default/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +6 -4
  95. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/common.test.ts +30 -0
  96. package/templates/default/specs/ApiInterception/TC-0012-direct-and-late-response/spec.md +22 -0
  97. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/common.test.ts +27 -0
  98. package/templates/default/specs/ApiInterception/TC-0013-real-network-call-recorded/spec.md +20 -0
  99. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/spec.md +24 -0
  100. package/templates/default/specs/ApiInterception/TC-0015-mock-server-sent-events/web.test.ts +26 -0
  101. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/spec.md +37 -0
  102. package/templates/default/specs/ApiInterception/TC-0016-bypass-websocket-with-app-level-mock/web.test.ts +21 -0
  103. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/spec.md +29 -0
  104. package/templates/default/specs/ApiInterception/TC-0017-bypass-captcha-with-test-mode-flag/web.test.ts +16 -0
  105. package/templates/default/specs/ApiInterception/suite.md +1 -0
  106. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/common.test.ts +30 -0
  107. package/templates/default/specs/ApiSeeding/TC-0014-seed-and-cleanup-via-hooks/spec.md +23 -0
  108. package/templates/default/specs/ApiSeeding/hooks/after/common.hook.ts +16 -0
  109. package/templates/default/specs/ApiSeeding/hooks/before/common.hook.ts +28 -0
  110. package/templates/default/specs/ApiSeeding/suite.md +9 -0
  111. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/mobile.test.ts +7 -0
  112. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/spec.md +19 -0
  113. package/templates/default/specs/Authentication/TC-0021-seed-authenticated-session/web.test.ts +9 -0
  114. package/templates/default/specs/Authentication/suite.md +9 -0
  115. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  116. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/spec.md +5 -0
  117. package/templates/default/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  118. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/mobile.test.ts +17 -0
  119. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/spec.md +28 -0
  120. package/templates/default/specs/BrowserContext/TC-0022-history-navigation/web.test.ts +26 -0
  121. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/common.test.ts +26 -0
  122. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/spec.md +7 -0
  123. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  124. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/spec.md +5 -0
  125. package/templates/default/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  126. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/common.test.ts +14 -0
  127. package/templates/default/specs/ContentMatchers/TC-0020-scoped-element-access/spec.md +21 -0
  128. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/mobile.test.ts +28 -0
  129. package/templates/default/specs/DeepLink/TC-0018-navigate-via-deep-link/spec.md +29 -0
  130. package/templates/default/specs/DeepLink/suite.md +9 -0
  131. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  132. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/spec.md +5 -0
  133. package/templates/default/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  134. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  135. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/spec.md +7 -0
  136. package/templates/default/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  137. package/templates/default/specs/ElementMatchers/suite.md +2 -1
  138. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  139. package/templates/default/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  140. package/templates/default/specs/EnvironmentConfig/suite.md +9 -0
  141. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/mobile.test.ts +26 -0
  142. package/templates/default/specs/PermissionRationale/TC-0010-camera-permission-allow/spec.md +22 -0
  143. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/mobile.test.ts +13 -0
  144. package/templates/default/specs/PermissionRationale/TC-0011-camera-permission-deny/spec.md +20 -0
  145. package/templates/default/specs/PermissionRationale/suite.md +9 -0
  146. package/templates/default/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  147. package/templates/default/specs/Playground/TC-0001-form-controls/spec.md +6 -0
  148. package/templates/default/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  149. package/templates/default/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  150. package/templates/default/specs/Playground/TC-0002-toggle-elements/spec.md +6 -0
  151. package/templates/default/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  152. package/templates/default/spectra.config.ts +1 -17
  153. package/templates/default/support/actions/fillCredentials/mobile.action.ts +3 -3
  154. package/templates/default/support/actions/fillCredentials/web.action.ts +1 -1
  155. package/templates/default/support/actions/seedSession/mobile.action.ts +9 -0
  156. package/templates/default/support/actions/seedSession/web.action.ts +13 -0
  157. package/templates/default/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  158. package/templates/default/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  159. package/templates/default/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  160. package/templates/default/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  161. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db +0 -0
  162. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-shm +0 -0
  163. package/templates/nx/.nx/workspace-data/70094CFD-E89B-57A1-9619-2FC5F4963C54.db-wal +0 -0
  164. package/templates/nx/.nx/workspace-data/d/daemon.log +5803 -168
  165. package/templates/nx/.nx/workspace-data/d/server-process.json +3 -0
  166. package/templates/nx/.nx/workspace-data/file-map.json +464 -0
  167. package/templates/nx/.nx/workspace-data/lockfile.hash +1 -0
  168. package/templates/nx/.nx/workspace-data/nx_files.nxt +0 -0
  169. package/templates/nx/.nx/workspace-data/parsed-lock-file.json +13485 -0
  170. package/templates/nx/.nx/workspace-data/project-graph.json +14388 -0
  171. package/templates/nx/package.json +3 -3
  172. package/templates/nx/packages/matchers/e2e/project.json +2 -2
  173. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
  174. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
  175. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
  176. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
  177. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  178. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  179. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  180. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
  181. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  182. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  183. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  184. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  185. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  186. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  187. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  188. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  189. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  190. package/templates/nx/packages/playground/e2e/project.json +2 -2
  191. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  192. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  193. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  194. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  195. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
  196. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
  197. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  198. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  199. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  200. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
  201. package/templates/nx/shared/testing/project.json +1 -2
  202. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  203. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  204. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  205. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  206. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  207. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  208. package/templates/nx/spectra.config.ts +1 -17
  209. package/templates/react-component/.cursorrules +17 -0
  210. package/templates/react-component/.vscode/extensions.json +5 -0
  211. package/templates/react-component/CLAUDE.md +14 -0
  212. package/templates/react-component/README.md +69 -0
  213. package/templates/react-component/fixtures/component-mock.json +12 -0
  214. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  215. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  216. package/templates/react-component/package.json +27 -0
  217. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  218. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  219. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  220. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  221. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  222. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  223. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  224. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  225. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  226. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  227. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  228. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  229. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  230. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  231. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  232. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  233. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  234. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  235. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  236. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  237. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  238. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  239. package/templates/react-component/spectra.config.ts +27 -0
  240. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  241. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  242. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  243. package/templates/react-component/src/test-utils.tsx +23 -0
  244. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  245. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  246. package/templates/react-component/tsconfig.json +8 -0
  247. package/bin/.testspectra-runner.hash +0 -1
  248. package/bin/testspectra-runner +0 -0
  249. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  250. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  251. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  252. package/dist/commands/cloud-run.d.ts +0 -13
  253. package/dist/commands/cloud-run.js +0 -120
  254. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  255. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  256. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- export class MatchersPage {
1
+ class MatchersPage {
2
2
  get visibleElement() { return Spectra.get("~visible-element"); }
3
3
  get hiddenElement() { return Spectra.get("~hidden-element"); }
4
4
  get existingElement() { return Spectra.get("~existing-element"); }
@@ -11,18 +11,27 @@ export class MatchersPage {
11
11
  get tokenInput() { return Spectra.get("~token-input"); }
12
12
  get exactText() { return Spectra.get("~exact-text"); }
13
13
  get partialText() { return Spectra.get("~partial-text"); }
14
- get activeTab() { return Spectra.get("~active-tab"); }
14
+ // `#` = native id (RN testID → resource-id). Its accessibility label deliberately reads
15
+ // "active-tab is-active" (RN exposes no CSS classes, so the class is carried in the a11y label
16
+ // for `shouldHaveClass`), so it isn't addressable by the a11y-id form `~active-tab`.
17
+ get activeTab() { return Spectra.get("#active-tab"); }
15
18
  get navLink() { return Spectra.get("~nav-link"); }
16
19
  get styledElement() { return Spectra.get("~styled-element"); }
17
20
  get focusInput() { return Spectra.get("~focus-input"); }
18
21
  get productCards() { return Spectra.getAll("~product-item-1, ~product-item-2, ~product-item-3, ~product-item-4"); }
19
22
  get emptyList() { return Spectra.getAll("~empty-list-item"); }
23
+ get overviewSection() { return Spectra.get("#overview-section"); }
24
+ // Scoped lookups via element.get() — resolved within overview-section's own bounds (Android
25
+ // has no ancestor API, so this is a bounds-containment lookup), not screen-wide.
26
+ overviewSubtitle() { return this.overviewSection.get("#demo-page-subtitle"); }
27
+ overviewOutsideElement() { return this.overviewSection.get("~submit-button"); }
20
28
 
21
29
  // API Demo elements
22
30
  get fetchApiButton() { return Spectra.get("~fetch-api-btn"); }
23
31
  get fetchStatusBadge() { return Spectra.get("~fetch-status-badge"); }
24
32
  get fetchedItems() { return Spectra.getAll("~api-post-item"); }
25
33
  get fetchLoadingIndicator() { return Spectra.get("~fetch-loading-indicator"); }
34
+ get fetchCustomUrlInput() { return Spectra.get("~fetch-custom-url"); }
26
35
  get postTitleInput() { return Spectra.get("~post-title-input"); }
27
36
  get postBodyInput() { return Spectra.get("~post-body-input"); }
28
37
  get submitPostButton() { return Spectra.get("~submit-post-btn"); }
@@ -32,9 +41,24 @@ export class MatchersPage {
32
41
 
33
42
  async open(section = "") {
34
43
  try {
44
+ // A fresh app process (relaunched by the orchestrator's bootstrap before every suite) has to
45
+ // fetch, parse, and execute the RN JS bundle from Metro before anything is on screen — on a
46
+ // cold start this can take well past a normal action's poll budget. Wait for the home screen
47
+ // itself first so that timing doesn't silently swallow the sidebar click below and leave the
48
+ // real failure to surface confusingly on whatever element the test touches next.
49
+ await Spectra.get("~toggle-sidebar-btn").waitForElement(30000);
50
+
35
51
  const isDrawerOpen = await Spectra.get("~close-sidebar-btn").isDisplayed().catch(() => false);
36
52
  if (!isDrawerOpen) {
37
53
  await Spectra.get("~toggle-sidebar-btn").click();
54
+ // The mobile drawer is a React Native `<Modal>` — a separate OS-level window with its own
55
+ // slide-in animation, not a plain re-rendered View — so its content (including every
56
+ // nav-item below) isn't in the accessibility tree at all until Android actually finishes
57
+ // creating that window, which is async and occasionally slow under load. Without this
58
+ // wait, clicking a nav item right after the toggle relies entirely on click()'s own
59
+ // poll-retry to bridge that gap, which can eat most of the ~10s implicit-wait budget when
60
+ // the Modal is slow to materialize.
61
+ await Spectra.get("~close-sidebar-btn").waitForElement(10000);
38
62
  }
39
63
  if (section) {
40
64
  await Spectra.get(`~nav-item-${section}`).click();
@@ -1,4 +1,4 @@
1
- export class MatchersPage {
1
+ class MatchersPage {
2
2
  get visibleElement() { return Spectra.get("#demo-visible-el"); }
3
3
  get hiddenElement() { return Spectra.get("#demo-hidden-el"); }
4
4
  get existingElement() { return Spectra.get("#demo-existing-el"); }
@@ -16,12 +16,18 @@ export class MatchersPage {
16
16
  get focusInput() { return Spectra.get("#demo-focus-input"); }
17
17
  get productCards() { return Spectra.getAll(".demo-product-card"); }
18
18
  get emptyList() { return Spectra.getAll("#demo-empty-list > *"); }
19
+ get overviewSection() { return Spectra.get("#overview"); }
20
+ // Scoped lookups via element.get() — resolved within overview-section's own subtree, not
21
+ // document-wide.
22
+ overviewSubtitle() { return this.overviewSection.get("#demo-page-subtitle"); }
23
+ overviewOutsideElement() { return this.overviewSection.get("#demo-submit-btn"); }
19
24
 
20
25
  // API Interception & Mocking elements
21
26
  get fetchApiButton() { return Spectra.get("#fetch-api-btn"); }
22
27
  get fetchStatusBadge() { return Spectra.get("#fetch-status-badge"); }
23
28
  get fetchedItems() { return Spectra.getAll(".api-fetched-item"); }
24
29
  get fetchLoadingIndicator() { return Spectra.get("#fetch-loading-indicator"); }
30
+ get fetchCustomUrlInput() { return Spectra.get("#fetch-custom-url-input"); }
25
31
  get postTitleInput() { return Spectra.get("#post-input-title"); }
26
32
  get postBodyInput() { return Spectra.get("#post-input-body"); }
27
33
  get submitPostButton() { return Spectra.get("#submit-post-btn"); }
@@ -30,7 +36,7 @@ export class MatchersPage {
30
36
  get postResponseJson() { return Spectra.get("#post-response-json"); }
31
37
 
32
38
  async open(section = "") {
33
- await browser.url("/" + (section ? "#" + section : ""));
39
+ await Spectra.navigate("/" + (section ? "#" + section : ""));
34
40
  }
35
41
  }
36
42
 
@@ -1,4 +1,4 @@
1
- export class NavigationPage {
1
+ class NavigationPage {
2
2
  get sidebarLinks() {
3
3
  return Spectra.getAll(
4
4
  "~nav-item-overview, ~nav-item-element-matchers, ~nav-item-visibility, ~nav-item-existence, ~nav-item-state, ~nav-item-value, ~nav-item-text, ~nav-item-class-attr, ~nav-item-browser-matchers, ~nav-item-url, ~nav-item-title, ~nav-item-collection-matchers, ~nav-item-length, ~nav-item-api-matchers, ~nav-item-api-fetch, ~nav-item-api-post, ~nav-item-playground"
@@ -1,4 +1,4 @@
1
- export class NavigationPage {
1
+ class NavigationPage {
2
2
  get sidebarLinks() { return Spectra.getAll("aside a"); }
3
3
  get overviewLink() { return Spectra.get("aside a[href='#overview']"); }
4
4
  get visibilityLink() { return Spectra.get("aside a[href='#visibility']"); }
@@ -1,14 +1,14 @@
1
- export class PlaygroundPage {
1
+ class PlaygroundPage {
2
2
  get visibleElement() { return Spectra.get("~playground-visible"); }
3
3
  get hiddenElement() { return Spectra.get("~playground-hidden"); }
4
4
  get existingElement() { return Spectra.get("~playground-existing"); }
5
5
  get removedElement() { return Spectra.get("~playground-removable"); }
6
6
  get enabledButton() { return Spectra.get("~enabled-button"); }
7
- get disabledButton() { return Spectra.get("~disabled-button"); }
8
- get termsCheckbox() { return Spectra.get("~terms-checkbox"); }
9
- get modeRadio() { return Spectra.get("~mode-radio"); }
10
- get emailInput() { return Spectra.get("~email-input"); }
11
- get tokenInput() { return Spectra.get("~token-input"); }
7
+ get disabledButton() { return Spectra.get("~pg-disabled-button"); }
8
+ get termsCheckbox() { return Spectra.get("~pg-terms-checkbox"); }
9
+ get modeRadio() { return Spectra.get("~pg-mode-radio"); }
10
+ get emailInput() { return Spectra.get("~pg-email-input"); }
11
+ get tokenInput() { return Spectra.get("~pg-token-input"); }
12
12
  get exactText() { return Spectra.get("~exact-text-el"); }
13
13
  get partialText() { return Spectra.get("~partial-text-el"); }
14
14
  get activeTab() { return Spectra.get("~active-tab-el"); }
@@ -20,9 +20,15 @@ export class PlaygroundPage {
20
20
 
21
21
  async open() {
22
22
  try {
23
+ // See MatchersPage.open()'s equivalent comment: waits for the cold-start home screen, then
24
+ // for the drawer Modal to actually finish materializing before clicking a nav item inside
25
+ // it — both races that otherwise surface as an occasional ~10s stall on the next click.
26
+ await Spectra.get("~toggle-sidebar-btn").waitForElement(30000);
27
+
23
28
  const isDrawerOpen = await Spectra.get("~close-sidebar-btn").isDisplayed().catch(() => false);
24
29
  if (!isDrawerOpen) {
25
30
  await Spectra.get("~toggle-sidebar-btn").click();
31
+ await Spectra.get("~close-sidebar-btn").waitForElement(10000);
26
32
  }
27
33
  await Spectra.get("~nav-item-playground").click();
28
34
  } catch {}
@@ -1,4 +1,4 @@
1
- export class PlaygroundPage {
1
+ class PlaygroundPage {
2
2
  get visibleElement() { return Spectra.get("#demo-visible-el-pg"); }
3
3
  get hiddenElement() { return Spectra.get("#demo-hidden-el-pg"); }
4
4
  get existingElement() { return Spectra.get("#demo-existing-el-pg"); }
@@ -19,7 +19,7 @@ export class PlaygroundPage {
19
19
  get emptyList() { return Spectra.getAll("#demo-empty-list-pg > *"); }
20
20
 
21
21
  async open() {
22
- await browser.url("/#playground");
22
+ await Spectra.navigate("/#playground");
23
23
  }
24
24
 
25
25
  async fillCredentials(email = Fixture.playgroundData.defaultEmail, token = Fixture.playgroundData.defaultToken) {
@@ -1,11 +1,53 @@
1
- it("should verify GET API fetch response items and state on Mobile", async () => {
1
+ it("Mock and assert GET API requests using browser.intercept", async () => {
2
2
  // Navigate to API Fetch section
3
3
  await MatchersPage.open("api-fetch");
4
4
 
5
+ // The app fetches the host demo server's self-hosted endpoint (http://localhost:5200/api/posts) —
6
+ // the device's global HTTP proxy (set up transparently at bootstrap) routes it through the
7
+ // worker's mock proxy without the app knowing or needing any special host/prefix, the same
8
+ // way the CDP web driver intercepts traffic for free. Clear any mock left registered by an
9
+ // earlier test in this same continuous session before adding this one.
10
+ Spectra.clearMocks();
11
+ const mock = await Spectra.intercept(
12
+ "api/posts",
13
+ "GET",
14
+ Fixture.apiData.mockPosts
15
+ );
16
+
5
17
  // Trigger fetch button in mobile demo app
6
18
  await MatchersPage.fetchApiButton.click();
7
19
 
8
- // Assert status badge and response items rendered
20
+ // Assert status and mocked elements
9
21
  await MatchersPage.fetchStatusBadge.shouldHaveText("200 OK");
10
- await MatchersPage.fetchedItems.shouldHaveLengthGreaterThan(0);
22
+ await MatchersPage.fetchedItems.shouldHaveLength(Fixture.apiData.mockPosts.length);
23
+ await MatchersPage.fetchedItems.first().shouldContainText(Fixture.apiData.mockPosts[0].title);
24
+
25
+ // Dynamically update fixture response
26
+ await mock.respondWith([
27
+ { id: 99, title: "Dynamic Updated Mock Title", body: "Updated on the fly" }
28
+ ]);
29
+ await MatchersPage.fetchApiButton.click();
30
+ await MatchersPage.fetchedItems.shouldHaveLength(1);
31
+ await MatchersPage.fetchedItems.first().shouldContainText("Dynamic Updated Mock Title");
32
+
33
+ // Reset to the base mock and queue a single one-time response — asserts a transient state
34
+ // (e.g. one retry attempt) without disturbing what every other call returns.
35
+ Spectra.clearMocks();
36
+ const queuedMock = await Spectra.intercept(
37
+ "api/posts",
38
+ "GET",
39
+ Fixture.apiData.mockPosts
40
+ );
41
+ await queuedMock.respondOnce([
42
+ { id: 777, title: "One-Time Queued Response", body: "Only served once" }
43
+ ]);
44
+
45
+ await MatchersPage.fetchApiButton.click();
46
+ await MatchersPage.fetchedItems.shouldHaveLength(1);
47
+ await MatchersPage.fetchedItems.first().shouldContainText("One-Time Queued Response");
48
+
49
+ // The queued response is consumed by the call above — this one reverts to the base rule.
50
+ await MatchersPage.fetchApiButton.click();
51
+ await MatchersPage.fetchedItems.shouldHaveLength(Fixture.apiData.mockPosts.length);
52
+ await MatchersPage.fetchedItems.first().shouldContainText(Fixture.apiData.mockPosts[0].title);
11
53
  });
@@ -10,3 +10,10 @@ coverage:
10
10
  ---
11
11
 
12
12
  # Mock and assert GET API requests using browser.intercept
13
+
14
+ ## Test Steps
15
+ 1. Open API fetch section in matchers page
16
+ 2. Intercept GET /api/posts with mock posts fixture data
17
+ 3. Click fetch API button
18
+ 4. Assert status badge displays 200 OK and rendered items match fixture
19
+ 5. Update mock response dynamically and verify updated items rendered
@@ -1,10 +1,14 @@
1
- it("should mock GET API response using browser.intercept and verify rendered items", async () => {
1
+ it("Mock and assert GET API requests using browser.intercept", async () => {
2
2
  // Navigate to API Fetch section
3
3
  await MatchersPage.open("api-fetch");
4
4
 
5
- // Setup API interception with Fixture data
6
- const mock = await browser.intercept(
7
- "/api/posts",
5
+ // The app fetches the demo server's own self-hosted endpoint (/api/posts) — served by the
6
+ // built-in demo app (started with `--demo`), so no external network is involved. Clear any mock
7
+ // left registered by an earlier test in this same continuous session before adding this one,
8
+ // since rules aren't scoped per-test and the first matching rule wins.
9
+ Spectra.clearMocks();
10
+ const mock = await Spectra.intercept(
11
+ "api/posts",
8
12
  "GET",
9
13
  Fixture.apiData.mockPosts
10
14
  );
@@ -0,0 +1,3 @@
1
+ it('Mock and assert POST API requests using browser.intercept', async () => {
2
+ // TODO: Implement test steps and assertions
3
+ });
@@ -0,0 +1,3 @@
1
+ it('Mock and assert POST API requests using browser.intercept', async () => {
2
+ // TODO: Implement test steps and assertions
3
+ });
@@ -1,7 +1,18 @@
1
- it("should verify POST API form submission and response payload on Mobile", async () => {
1
+ it("Mock and assert POST API requests using browser.intercept", async () => {
2
2
  // Navigate to API POST section
3
3
  await MatchersPage.open("api-post");
4
4
 
5
+ // Setup POST API interception with status 201 — the app posts to the host demo server's
6
+ // self-hosted endpoint (http://localhost:5200/api/posts), transparently routed through the
7
+ // worker's mock proxy via the device's global HTTP proxy set up at bootstrap.
8
+ Spectra.clearMocks();
9
+ await Spectra.intercept(
10
+ "api/posts",
11
+ "POST",
12
+ Fixture.apiData.mockPostResponse,
13
+ { statusCode: 201 }
14
+ );
15
+
5
16
  // Fill in form fields from Fixture
6
17
  await MatchersPage.postTitleInput.type(Fixture.apiData.mockNewPost.title);
7
18
  await MatchersPage.postBodyInput.type(Fixture.apiData.mockNewPost.body);
@@ -9,7 +20,7 @@ it("should verify POST API form submission and response payload on Mobile", asyn
9
20
  // Submit POST form
10
21
  await MatchersPage.submitPostButton.click();
11
22
 
12
- // Assert response elements and status badge
23
+ // Assert response elements and status
13
24
  await MatchersPage.postStatusBadge.shouldHaveText("201 Created");
14
25
  await MatchersPage.postSuccessBanner.shouldBeVisible();
15
26
  await MatchersPage.postResponseJson.shouldContainText(Fixture.apiData.mockNewPost.title);
@@ -10,3 +10,10 @@ coverage:
10
10
  ---
11
11
 
12
12
  # Mock and assert POST API requests using browser.intercept
13
+
14
+ ## Test Steps
15
+ 1. Open API POST section in matchers page
16
+ 2. Intercept POST /api/posts with mock response returning 201 status
17
+ 3. Fill post title and post body inputs from fixture
18
+ 4. Click submit POST button
19
+ 5. Assert status badge displays 201 Created and response JSON contains new title
@@ -1,10 +1,12 @@
1
- it("should mock POST API submission using browser.intercept and verify created response", async () => {
1
+ it("Mock and assert POST API requests using browser.intercept", async () => {
2
2
  // Navigate to API POST section
3
3
  await MatchersPage.open("api-post");
4
4
 
5
- // Setup POST API interception with status 201
6
- await browser.intercept(
7
- "/api/posts",
5
+ // Setup POST API interception with status 201 — the app posts to the demo server's own
6
+ // self-hosted endpoint (/api/posts), no external network involved.
7
+ Spectra.clearMocks();
8
+ await Spectra.intercept(
9
+ "api/posts",
8
10
  "POST",
9
11
  Fixture.apiData.mockPostResponse,
10
12
  { statusCode: 201 }
@@ -0,0 +1,30 @@
1
+ it("Intercept a direct (immediate) response and a late (delayed) response using Fixture data", async () => {
2
+ await MatchersPage.open("api-fetch");
3
+
4
+ // The app fetches the self-hosted demo endpoint (/api/posts on web, the same path served by
5
+ // the demo server and reached through the device proxy on mobile) — no external network, so
6
+ // there's no upstream latency leaking into the "immediate" assertion below. On web, CDP's
7
+ // Fetch domain intercepts it; on Android, the device's global HTTP proxy routes it through the
8
+ // same worker-side mock engine. Neither platform needs the app, or this test, to know anything
9
+ // special about being under test.
10
+ Spectra.clearMocks();
11
+
12
+ // Direct (immediate) response — Fixture data returned with no artificial latency.
13
+ const mock = await Spectra.intercept("api/posts", "GET", Fixture.apiData.mockPosts);
14
+ await MatchersPage.fetchApiButton.click();
15
+ await MatchersPage.fetchStatusBadge.shouldHaveText("200 OK");
16
+ await MatchersPage.fetchedItems.shouldHaveLength(Fixture.apiData.mockPosts.length);
17
+ await MatchersPage.fetchedItems.first().shouldContainText(Fixture.apiData.mockPosts[0].title);
18
+
19
+ // Late (delayed) response — same mock rule, switched to hold the response back via `delayMs`.
20
+ // The loading indicator must still be visible right after the click, proving this is real
21
+ // network-level latency (the request genuinely stays pending) rather than a client-side-only
22
+ // spinner timer. The delay is set past the default assertion timeout on purpose, to demonstrate
23
+ // the per-assertion `{ timeoutMs }` override below — without it, these assertions would fail-fast
24
+ // before the delayed response ever arrives.
25
+ await mock.respondWith(Fixture.apiData.mockPosts, { delayMs: 2500 });
26
+ await MatchersPage.fetchApiButton.click();
27
+ await MatchersPage.fetchLoadingIndicator.shouldBeVisible();
28
+ await MatchersPage.fetchStatusBadge.shouldHaveText("200 OK", { timeoutMs: 3500 });
29
+ await MatchersPage.fetchedItems.shouldHaveLength(Fixture.apiData.mockPosts.length, { timeoutMs: 3500 });
30
+ });
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: TC-0012-direct-and-late-response
3
+ title: Intercept a direct (immediate) response and a late (delayed) response using Fixture data
4
+ priority: High
5
+ caseType: Positive
6
+ status: ready-for-automation
7
+ coverage:
8
+ web: automated
9
+ mobile: automated
10
+ ---
11
+
12
+ # Intercept a direct (immediate) response and a late (delayed) response using Fixture data
13
+
14
+ ## Test Steps
15
+ 1. Open the API fetch section in the matchers page
16
+ 2. Intercept GET /api/posts with mock posts fixture data
17
+ 3. Click the fetch API button
18
+ 4. Assert the status badge displays 200 OK and the immediate fixture items render
19
+ 5. Update the mock response with an artificial 50ms delay
20
+ 6. Click the fetch API button
21
+ 7. Assert the loading indicator is visible while the request is held
22
+ 8. Assert the status badge displays 200 OK and the delayed fixture items render
@@ -0,0 +1,27 @@
1
+ it("Make a genuine (non-intercepted) network call and verify it is recorded", async () => {
2
+ await MatchersPage.open("api-fetch");
3
+
4
+ // Deliberately no Spectra.intercept() call — this is a genuine request that actually leaves
5
+ // the app and hits a real server, not a mock. It targets the demo server's own self-hosted
6
+ // endpoint (started with `--demo`) instead of an external public API, so it's deterministic
7
+ // and fast everywhere. On web the browser reaches localhost directly; on Android it's
8
+ // transparently routed through the device's global HTTP proxy with no matching mock rule, so
9
+ // the worker's proxy just forwards it for real. Clear any mock a previous test in this suite
10
+ // left registered (e.g. against /api/posts) so it can't accidentally catch this request too —
11
+ // this test uses a different resource (/api/todos/1) specifically to avoid that collision,
12
+ // plus a cache-busting query param so a repeat run doesn't get a 304 Not Modified from the
13
+ // device/browser's own HTTP cache.
14
+ Spectra.clearMocks();
15
+ const cacheBuster = Date.now();
16
+ await MatchersPage.fetchCustomUrlInput.type(`http://localhost:5200/api/todos/1?cb=${cacheBuster}`);
17
+ await MatchersPage.fetchApiButton.click();
18
+ await MatchersPage.fetchStatusBadge.shouldHaveText("200 OK");
19
+
20
+ const entry = Spectra.browser.recordedNetwork.find((e) => e.url.includes(`todos/1?cb=${cacheBuster}`));
21
+ if (!entry) {
22
+ throw new Error("Expected a recorded network entry for the real demo-server call, found none");
23
+ }
24
+ if (entry.status !== 200) {
25
+ throw new Error(`Expected recorded entry status 200, got ${entry.status}`);
26
+ }
27
+ });
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: TC-0013-real-network-call-recorded
3
+ title: Make a genuine (non-intercepted) network call and verify it is recorded
4
+ priority: High
5
+ caseType: Positive
6
+ status: ready-for-automation
7
+ coverage:
8
+ web: automated
9
+ mobile: automated
10
+ ---
11
+
12
+ # Make a genuine (non-intercepted) network call and verify it is recorded
13
+
14
+ ## Test Steps
15
+ 1. Open the API fetch section in the matchers page
16
+ 2. Clear any active mocks left by a previous test
17
+ 3. Enter a real (non-intercepted) demo-server todo endpoint URL with a cache-busting query param
18
+ 4. Click the fetch API button
19
+ 5. Assert the status badge displays 200 OK
20
+ 6. Assert the network report records the genuine call with status 200
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: TC-0015-mock-server-sent-events
3
+ title: Mock a Server-Sent Events (EventSource) stream via the existing network-intercept engine
4
+ priority: Medium
5
+ caseType: Positive
6
+ status: ready-for-automation
7
+ coverage:
8
+ web: automated
9
+ mobile: unsupported
10
+ ---
11
+
12
+ # Mock a Server-Sent Events (EventSource) stream via the existing network-intercept engine
13
+
14
+ Mobile coverage is `unsupported`: React Native has no built-in `EventSource` — a real project
15
+ targeting mobile would add a userland SSE client library, at which point whether/how it's
16
+ mockable depends entirely on that library's own networking implementation, not something
17
+ TestSpectra's driver can generalize over.
18
+
19
+ ## Test Steps
20
+ 1. Open the SSE matcher section
21
+ 2. Intercept GET /api/sse-feed with a text/event-stream mock body
22
+ 3. Click the connect button
23
+ 4. Assert two SSE messages render
24
+ 5. Assert the first message contains "hello from mock SSE" and the last contains "second event"
@@ -0,0 +1,26 @@
1
+ it("Mock a Server-Sent Events (EventSource) stream via the existing network-intercept engine", async () => {
2
+ await MatchersPage.open("sse");
3
+
4
+ // EventSource's initial request is a plain HTTP GET under the hood — CDP's Fetch domain
5
+ // intercepts it exactly like any other fetch()/XHR request. Fulfilling it with a
6
+ // text/event-stream body delivers every "data: ...\n\n" block at once; the browser's own
7
+ // EventSource parser still splits that into individual "message" events and dispatches them
8
+ // just as it would for a real stream, which is the part this demo actually cares about.
9
+ //
10
+ // What this can't fake: `Fetch.fulfillRequest` delivers a complete, connection-closing
11
+ // response (there's no way to keep it open indefinitely the way a real SSE endpoint would), so
12
+ // the browser's EventSource treats that closure as a connection error shortly after — the
13
+ // events still arrive and get processed first, but the app's own "CONNECTED" status flips back
14
+ // to disconnected right after, same as it would after a real server unexpectedly closed the
15
+ // connection. So this test verifies the events were received, not that the connection stayed
16
+ // open — genuinely-persistent SSE isn't something a request/response-shaped mock can simulate.
17
+ await Spectra.intercept("/api/sse-feed", "GET", {
18
+ body: 'data: {"text":"hello from mock SSE"}\n\ndata: {"text":"second event"}\n\n',
19
+ headers: { "Content-Type": "text/event-stream" },
20
+ });
21
+
22
+ await Spectra.get("#sse-connect-btn").click();
23
+ await Spectra.getAll(".sse-message-item").shouldHaveLength(2);
24
+ await Spectra.getAll(".sse-message-item").first().shouldContainText("hello from mock SSE");
25
+ await Spectra.getAll(".sse-message-item").last().shouldContainText("second event");
26
+ });
@@ -0,0 +1,37 @@
1
+ ---
2
+ id: TC-0016-bypass-websocket-with-app-level-mock
3
+ title: Bypass a real WebSocket connection via an app-level test-only mock flag
4
+ priority: Medium
5
+ caseType: Positive
6
+ status: ready-for-automation
7
+ coverage:
8
+ web: automated
9
+ mobile: unsupported
10
+ ---
11
+
12
+ # Bypass a real WebSocket connection via an app-level test-only mock flag
13
+
14
+ **Why this isn't network-level interception like `Spectra.intercept()`:** a WebSocket connection
15
+ starts as an HTTP `Upgrade` handshake and then becomes a raw bidirectional frame stream — it's
16
+ never a single request/response CDP's `Fetch` domain can fulfill the way it fulfills a normal
17
+ `fetch()`/`XHR`/`EventSource` request. Chrome DevTools Protocol's WebSocket-related events
18
+ (`Network.webSocketCreated`, `webSocketFrameSent`/`webSocketFrameReceived`, etc.) are
19
+ observation-only — CDP has no command that mocks or rewrites a WS frame in flight. This is a
20
+ genuine protocol-level limitation, not a gap in this driver.
21
+
22
+ The practical workaround, and what this demo shows: the app itself checks for a test-only global
23
+ flag (`window.__mockWebSocketMessages`) and swaps in an in-memory fake socket when present,
24
+ instead of opening a real connection. `Spectra.browser.evaluate()` sets that flag before the app
25
+ connects. This is the same pattern used by Cypress/Playwright test suites for the same reason.
26
+
27
+ Mobile coverage is `unsupported`: React Native's WebSocket implementation is a native module,
28
+ not a page-context global reachable via a browser-style `evaluate()` call — a mobile equivalent
29
+ would need the RN app's own WebSocket client wrapper to support a comparable test-only bypass.
30
+
31
+ ## Test Steps
32
+ 1. Open the WebSocket matcher section
33
+ 2. Set the app-level mock flag via browser.evaluate before the app connects
34
+ 3. Click the connect button
35
+ 4. Assert the connection status displays CONNECTED and two mocked frames render
36
+ 5. Click the disconnect button
37
+ 6. Assert the connection status displays DISCONNECTED
@@ -0,0 +1,21 @@
1
+ it("Bypass a real WebSocket connection via an app-level test-only mock flag", async () => {
2
+ await MatchersPage.open("websocket");
3
+
4
+ // Set BEFORE the app opens its connection — the component checks this flag inside its own
5
+ // connect() handler and, if present, uses an in-memory fake socket instead of a real
6
+ // `new WebSocket(...)`. This is necessarily app-level, not network-level: see this TC's
7
+ // spec.md for why CDP has no way to intercept/mock a WebSocket handshake or its frames the
8
+ // way Spectra.intercept() does for fetch()/XHR/EventSource.
9
+ await Spectra.browser.evaluate(
10
+ "window.__mockWebSocketMessages = ['hello from mock WS', 'second frame']",
11
+ );
12
+
13
+ await Spectra.get("#ws-connect-btn").click();
14
+ await Spectra.get("#ws-connection-status").shouldHaveText("CONNECTED");
15
+ await Spectra.getAll(".ws-message-item").shouldHaveLength(2);
16
+ await Spectra.getAll(".ws-message-item").first().shouldContainText("hello from mock WS");
17
+ await Spectra.getAll(".ws-message-item").last().shouldContainText("second frame");
18
+
19
+ await Spectra.get("#ws-disconnect-btn").click();
20
+ await Spectra.get("#ws-connection-status").shouldHaveText("DISCONNECTED");
21
+ });
@@ -0,0 +1,29 @@
1
+ ---
2
+ id: TC-0017-bypass-captcha-with-test-mode-flag
3
+ title: Bypass a live reCAPTCHA widget via an app-level test-only flag
4
+ priority: Medium
5
+ caseType: Positive
6
+ status: ready-for-automation
7
+ coverage:
8
+ web: automated
9
+ mobile: unsupported
10
+ ---
11
+
12
+ # Bypass a live reCAPTCHA widget via an app-level test-only flag
13
+
14
+ Real reCAPTCHA renders inside a cross-origin iframe (`google.com`), which Google explicitly
15
+ discourages driving via browser automation — there's no `Spectra.intercept()`-style network mock
16
+ for a live third-party widget's challenge/response flow. The demo embeds the real widget (site key
17
+ `6LcWK4QtAAAAAAeA8IAN20JceGQ7L-RWE-LfhZnB`) for visual completeness, but the test itself uses the
18
+ same pattern real projects use in CI: an app-level `window.__bypassCaptcha` flag the submit
19
+ handler honors instead of ever attempting to automate the actual challenge.
20
+
21
+ Mobile coverage is `unsupported`: a native captcha SDK (rather than a page-context script tag)
22
+ would need its own test-only bypass hook, which is outside what a generic web-style demo can show.
23
+
24
+ ## Test Steps
25
+ 1. Open the captcha matcher section
26
+ 2. Click submit without the bypass flag and assert the widget refuses
27
+ 3. Set the app-level bypass flag via browser.evaluate
28
+ 4. Click submit again
29
+ 5. Assert the submission message indicates the form was bypassed for automated testing
@@ -0,0 +1,16 @@
1
+ it("Bypass a live reCAPTCHA widget via an app-level test-only flag", async () => {
2
+ await MatchersPage.open("captcha");
3
+
4
+ // Baseline: without the flag (and no completed challenge), the widget refuses to submit —
5
+ // proving the success below is specifically driven by the bypass flag, not a stale state.
6
+ await Spectra.get("#captcha-submit-btn").click();
7
+ await Spectra.get("#captcha-submit-message").shouldContainText("complete the reCAPTCHA challenge");
8
+
9
+ // Set BEFORE submitting — see this TC's spec.md for why this has to be an app-level bypass
10
+ // rather than a network-level mock (real reCAPTCHA lives in a cross-origin iframe with no
11
+ // request/response Spectra.intercept() could fulfill).
12
+ await Spectra.browser.evaluate("window.__bypassCaptcha = true");
13
+
14
+ await Spectra.get("#captcha-submit-btn").click();
15
+ await Spectra.get("#captcha-submit-message").shouldContainText("bypassed for automated test");
16
+ });
@@ -7,3 +7,4 @@ parallel: true
7
7
  ---
8
8
 
9
9
  # Suite: ApiInterception
10
+
@@ -0,0 +1,30 @@
1
+ // The `before` hook (hooks/before/common.hook.ts) already ran by the time this test body
2
+ // executes — it seeded a post via a real POST to the demo server's self-hosted API and stashed
3
+ // the result on globalThis for this test to read. Cleanup (hooks/after/common.hook.ts) runs
4
+ // after this whole suite finishes, so it can't be observed from inside the suite's own tests —
5
+ // a separate monitoring suite/run would be how a real project verifies cleanup actually happened.
6
+ //
7
+ // Suite-scoped hooks only run when a worker's whole batch is unambiguously scoped to this one
8
+ // suite (e.g. `spectra run specs/ApiSeeding` or targeting this TC directly) — hook functions are
9
+ // process-global singleton slots, not per-test-case, so a worker batch spanning multiple suites
10
+ // has no correct way to scope suite A's hooks to only run around suite A's own tests, and skips
11
+ // suite-level hooks entirely rather than risk applying the wrong suite's hooks to the wrong
12
+ // tests. Running the FULL default template (`spectra run -t web`/`android`, every suite at once)
13
+ // will legitimately fail this specific test for that reason — that's an inherent limitation of
14
+ // the current worker-batching model, not a bug in this demo.
15
+ it("Verify test data seeded by a before hook via the demo server's self-hosted API is available to the test", async () => {
16
+ const seeding = (globalThis as any).__apiSeeding;
17
+
18
+ if (!seeding?.seededPost) {
19
+ throw new Error("Expected the before hook to have seeded a post onto globalThis.__apiSeeding, found none");
20
+ }
21
+ if (typeof seeding.seededPost.id !== "number") {
22
+ throw new Error(`Expected seeded post to have a numeric id, got ${JSON.stringify(seeding.seededPost.id)}`);
23
+ }
24
+ if (seeding.seededPost.title !== "TestSpectra seeded post") {
25
+ throw new Error(`Expected seeded post title to match, got "${seeding.seededPost.title}"`);
26
+ }
27
+ if (seeding.cleanedUp !== false) {
28
+ throw new Error("Expected cleanedUp to still be false — the after hook hasn't run yet at this point");
29
+ }
30
+ });