@testspectra/cli 1.1.6 → 1.1.8-rc.1

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 (251) hide show
  1. package/bin/.testspectra-runner.hash +1 -1
  2. package/bin/testspectra-runner +0 -0
  3. package/demo-app/assets/index-BhlW-eXG.css +1 -0
  4. package/demo-app/assets/index-DOtRypCa.js +154 -0
  5. package/demo-app/index.html +2 -2
  6. package/dist/.tsbuildinfo +1 -1
  7. package/dist/commands/__tests__/run-e2e.test.js +254 -13
  8. package/dist/commands/__tests__/run.test.js +92 -16
  9. package/dist/commands/demo.js +17 -7
  10. package/dist/commands/init.d.ts +1 -1
  11. package/dist/commands/init.js +6 -1
  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 +76 -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 +1 -1
  29. package/dist/generator/tsconfig-generator.js +44 -16
  30. package/dist/generator/type-generator.d.ts +24 -1
  31. package/dist/generator/type-generator.js +104 -8
  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 +17 -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 +34 -21
  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 +29 -25
  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 +5388 -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 +14389 -0
  171. package/templates/nx/package.json +3 -3
  172. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/mobile.test.ts +25 -1
  173. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0008-fetch-mocking/web.test.ts +2 -2
  174. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/mobile.test.ts +22 -1
  175. package/templates/nx/packages/matchers/e2e/specs/ApiInterception/TC-0009-post-mocking/web.test.ts +2 -2
  176. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/mobile.test.ts +1 -1
  177. package/templates/nx/packages/matchers/e2e/specs/BrowserContext/TC-0007-title-and-navigation/web.test.ts +1 -1
  178. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +1 -1
  179. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +1 -1
  180. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/mobile.test.ts +1 -1
  181. package/templates/nx/packages/matchers/e2e/specs/ContentMatchers/TC-0006-collections-and-empty/web.test.ts +1 -1
  182. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/mobile.test.ts +1 -1
  183. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0003-visibility-and-dom/web.test.ts +1 -1
  184. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/mobile.test.ts +1 -1
  185. package/templates/nx/packages/matchers/e2e/specs/ElementMatchers/TC-0004-states-and-attributes/web.test.ts +1 -1
  186. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/common.test.ts +5 -0
  187. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/TC-0019-read-custom-environment-variable/spec.md +16 -0
  188. package/templates/nx/packages/matchers/e2e/specs/EnvironmentConfig/suite.md +9 -0
  189. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/mobile.test.ts +1 -1
  190. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0001-form-controls/web.test.ts +1 -1
  191. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/mobile.test.ts +1 -1
  192. package/templates/nx/packages/playground/e2e/specs/Playground/TC-0002-toggle-elements/web.test.ts +1 -1
  193. package/templates/nx/shared/testing/page-objects/MatchersPage/mobile.ts +1 -1
  194. package/templates/nx/shared/testing/page-objects/MatchersPage/web.ts +83 -29
  195. package/templates/nx/shared/testing/page-objects/NavigationPage/mobile.ts +1 -1
  196. package/templates/nx/shared/testing/page-objects/NavigationPage/web.ts +1 -1
  197. package/templates/nx/shared/testing/page-objects/PlaygroundPage/mobile.ts +1 -1
  198. package/templates/nx/shared/testing/page-objects/PlaygroundPage/web.ts +2 -2
  199. package/templates/nx/shared/testing/support/actions/fillCredentials/mobile.action.ts +1 -1
  200. package/templates/nx/shared/testing/support/actions/fillCredentials/web.action.ts +1 -1
  201. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/mobile.step.ts +1 -1
  202. package/templates/nx/shared/testing/support/steps/togglePlaygroundStates/web.step.ts +1 -1
  203. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/mobile.step.ts +1 -1
  204. package/templates/nx/shared/testing/support/steps/verifyPlaygroundState/web.step.ts +1 -1
  205. package/templates/nx/spectra.config.ts +1 -17
  206. package/templates/react-component/.cursorrules +17 -0
  207. package/templates/react-component/.vscode/extensions.json +5 -0
  208. package/templates/react-component/CLAUDE.md +14 -0
  209. package/templates/react-component/README.md +69 -0
  210. package/templates/react-component/fixtures/component-mock.json +12 -0
  211. package/templates/react-component/global-hooks/after/web.hook.ts +3 -0
  212. package/templates/react-component/global-hooks/before/web.hook.ts +3 -0
  213. package/templates/react-component/package.json +27 -0
  214. package/templates/react-component/page-objects/BadgeComponent/web.ts +23 -0
  215. package/templates/react-component/page-objects/ButtonComponent/web.ts +23 -0
  216. package/templates/react-component/page-objects/InputComponent/web.ts +13 -0
  217. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/spec.md +18 -0
  218. package/templates/react-component/specs/BadgeComponent/TC-0002-badge-severities-and-dot/web.test.tsx +11 -0
  219. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/spec.md +18 -0
  220. package/templates/react-component/specs/BadgeComponent/TC-0006-badge-success-with-dot/web.test.tsx +10 -0
  221. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/spec.md +18 -0
  222. package/templates/react-component/specs/BadgeComponent/TC-0007-badge-by-severity-lookup/web.test.tsx +16 -0
  223. package/templates/react-component/specs/BadgeComponent/suite.md +12 -0
  224. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/spec.md +19 -0
  225. package/templates/react-component/specs/ButtonComponent/TC-0001-button-rendering-and-click/web.test.tsx +28 -0
  226. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/spec.md +19 -0
  227. package/templates/react-component/specs/ButtonComponent/TC-0004-disabled-button-not-clickable/web.test.tsx +22 -0
  228. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/spec.md +18 -0
  229. package/templates/react-component/specs/ButtonComponent/TC-0005-button-renders-icon/web.test.tsx +8 -0
  230. package/templates/react-component/specs/ButtonComponent/suite.md +12 -0
  231. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/spec.md +20 -0
  232. package/templates/react-component/specs/InputComponent/TC-0003-input-typing-and-clear/web.test.tsx +28 -0
  233. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/spec.md +18 -0
  234. package/templates/react-component/specs/InputComponent/TC-0008-disabled-input-not-editable/web.test.tsx +9 -0
  235. package/templates/react-component/specs/InputComponent/suite.md +12 -0
  236. package/templates/react-component/spectra.config.ts +27 -0
  237. package/templates/react-component/src/components/Badge/Badge.tsx +60 -0
  238. package/templates/react-component/src/components/Button/Button.tsx +57 -0
  239. package/templates/react-component/src/components/Input/Input.tsx +41 -0
  240. package/templates/react-component/src/test-utils.tsx +23 -0
  241. package/templates/react-component/support/actions/fillInputField/web.action.ts +9 -0
  242. package/templates/react-component/support/steps/verifyButtonState/web.step.ts +12 -0
  243. package/templates/react-component/tsconfig.json +8 -0
  244. package/demo-app/assets/index-DPfv5_EF.js +0 -116
  245. package/demo-app/assets/index-gIwu2EMV.css +0 -1
  246. package/dist/commands/__tests__/cloud-run.test.js +0 -93
  247. package/dist/commands/cloud-run.d.ts +0 -13
  248. package/dist/commands/cloud-run.js +0 -120
  249. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/mobile.test.ts +0 -17
  250. package/templates/default/specs/ContentMatchers/TC-0005-text-and-values/web.test.ts +0 -17
  251. /package/dist/{commands/__tests__/cloud-run.test.d.ts → linter/__tests__/anonymous-export.test.d.ts} +0 -0
@@ -0,0 +1,115 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { SpecLinter } from '../spec-linter.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ /**
9
+ * Covers the component-testing `.tsx` import exception (see
10
+ * docs/v2/features/component-testing/react/mount-convention.md §"Static Imports Are Allowed
11
+ * Here"): imports are allowed in `.test.tsx` files EXCEPT ones resolving into ambient-global-
12
+ * backed directories (page-objects/, support/steps/, fixtures/). Structural rules
13
+ * (single-it-required, no-describe-block) stay fully enforced for `.tsx` exactly as for `.ts`.
14
+ */
15
+ describe('Component Test (.tsx) Import Exception', () => {
16
+ const tempDir = path.join(__dirname, '__temp_tsx_imports__');
17
+ const specFrontmatter = `---
18
+ id: TC-0200
19
+ title: Sample component test
20
+ priority: High
21
+ caseType: Positive
22
+ status: automated
23
+ coverage:
24
+ web: automated
25
+ ---
26
+
27
+ # Sample component test
28
+
29
+ ## Test Steps
30
+ 1. Mount the component and assert it is visible.
31
+ `;
32
+ beforeEach(() => {
33
+ fs.mkdirSync(tempDir, { recursive: true });
34
+ fs.writeFileSync(path.join(tempDir, 'spec.md'), specFrontmatter);
35
+ });
36
+ afterEach(() => {
37
+ if (fs.existsSync(tempDir)) {
38
+ fs.rmSync(tempDir, { recursive: true, force: true });
39
+ }
40
+ });
41
+ it('does not flag no-manual-import for ordinary component/library imports in .test.tsx', () => {
42
+ fs.writeFileSync(path.join(tempDir, 'web.test.tsx'), `import { Button } from '../../src/components/Button/Button.js';
43
+ import React from 'react';
44
+
45
+ it('Sample component test', async () => {
46
+ await Spectra.mount(<Button label="Save" />);
47
+ });
48
+ `);
49
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
50
+ const importErrors = diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
51
+ expect(importErrors).toHaveLength(0);
52
+ });
53
+ it('still flags no-manual-import when a .test.tsx import resolves into page-objects/', () => {
54
+ fs.writeFileSync(path.join(tempDir, 'web.test.tsx'), `import ButtonComponent from '../../page-objects/ButtonComponent/web.js';
55
+
56
+ it('Sample component test', async () => {
57
+ await ButtonComponent.root.shouldBeVisible();
58
+ });
59
+ `);
60
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
61
+ const importErrors = diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
62
+ expect(importErrors).toHaveLength(1);
63
+ expect(importErrors[0].message).toContain('page-objects');
64
+ });
65
+ it('still flags no-manual-import when a .test.tsx import resolves into support/steps/', () => {
66
+ fs.writeFileSync(path.join(tempDir, 'web.test.tsx'), `import verifyButtonState from '../../support/steps/verifyButtonState/web.step.js';
67
+
68
+ it('Sample component test', async () => {
69
+ await verifyButtonState('Save', true);
70
+ });
71
+ `);
72
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
73
+ const importErrors = diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
74
+ expect(importErrors).toHaveLength(1);
75
+ });
76
+ it('still flags no-manual-import when a .test.tsx import resolves into fixtures/', () => {
77
+ fs.writeFileSync(path.join(tempDir, 'web.test.tsx'), `import users from '../../fixtures/users.json';
78
+
79
+ it('Sample component test', async () => {
80
+ expect(users).toBeDefined();
81
+ });
82
+ `);
83
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
84
+ const importErrors = diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
85
+ expect(importErrors).toHaveLength(1);
86
+ });
87
+ it('still enforces single-it-required and no-describe-block in .test.tsx files', () => {
88
+ fs.writeFileSync(path.join(tempDir, 'web.test.tsx'), `import { Button } from '../../src/components/Button/Button.js';
89
+
90
+ describe('Button', () => {
91
+ it('Sample component test', async () => {
92
+ await Spectra.mount(<Button label="Save" />);
93
+ });
94
+
95
+ it('Another test', async () => {
96
+ await Spectra.mount(<Button label="Cancel" />);
97
+ });
98
+ });
99
+ `);
100
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
101
+ expect(diagnostics.some((d) => d.code === 'testspectra/no-describe-block')).toBe(true);
102
+ expect(diagnostics.some((d) => d.code === 'testspectra/single-it-required')).toBe(true);
103
+ });
104
+ it('still forbids all imports (unchanged behavior) in plain .test.ts E2E specs', () => {
105
+ fs.writeFileSync(path.join(tempDir, 'web.test.ts'), `import { LoginPage } from '../../page-objects/LoginPage/web.js';
106
+
107
+ it('Sample component test', async () => {
108
+ await LoginPage.submitButton.click();
109
+ });
110
+ `);
111
+ const { diagnostics } = SpecLinter.validateSpecFile(path.join(tempDir, 'spec.md'));
112
+ const importErrors = diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
113
+ expect(importErrors).toHaveLength(1);
114
+ });
115
+ });
@@ -0,0 +1,270 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { SpecLinter } from '../spec-linter.js';
6
+ import { isInsideSharedLibrary, findSharedLibraryRoots } from '../discovery.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ describe('Shared Library Boundary (testspectra/no-specs-in-shared-lib)', () => {
10
+ const tempDir = path.join(__dirname, '__temp_shared_boundary__');
11
+ beforeEach(() => {
12
+ fs.mkdirSync(tempDir, { recursive: true });
13
+ });
14
+ afterEach(() => {
15
+ if (fs.existsSync(tempDir)) {
16
+ fs.rmSync(tempDir, { recursive: true, force: true });
17
+ }
18
+ });
19
+ describe('findSharedLibraryRoots()', () => {
20
+ it('discovers parent folders of project.json files containing both scope:shared and type:shared', () => {
21
+ const sharedDir1 = path.join(tempDir, 'shared', 'testing');
22
+ const sharedDir2 = path.join(tempDir, 'libs', 'common-helpers');
23
+ const nonSharedDir = path.join(tempDir, 'packages', 'playground', 'e2e');
24
+ fs.mkdirSync(sharedDir1, { recursive: true });
25
+ fs.mkdirSync(sharedDir2, { recursive: true });
26
+ fs.mkdirSync(nonSharedDir, { recursive: true });
27
+ fs.writeFileSync(path.join(sharedDir1, 'project.json'), JSON.stringify({ name: 'shared-testing', tags: ['scope:shared', 'type:shared'] }));
28
+ fs.writeFileSync(path.join(sharedDir2, 'project.json'), JSON.stringify({ name: 'common-helpers', tags: ['scope:shared', 'type:shared'] }));
29
+ fs.writeFileSync(path.join(nonSharedDir, 'project.json'), JSON.stringify({ name: 'playground-e2e', tags: ['scope:playground', 'type:e2e'] }));
30
+ const roots = findSharedLibraryRoots(tempDir);
31
+ expect(roots).toHaveLength(2);
32
+ expect(roots).toContain(sharedDir1);
33
+ expect(roots).toContain(sharedDir2);
34
+ expect(roots).not.toContain(nonSharedDir);
35
+ });
36
+ });
37
+ describe('isInsideSharedLibrary()', () => {
38
+ it('returns true for files inside a project tagged with scope:shared or type:shared in project.json', () => {
39
+ const sharedDir = path.join(tempDir, 'shared', 'testing');
40
+ fs.mkdirSync(sharedDir, { recursive: true });
41
+ fs.writeFileSync(path.join(sharedDir, 'project.json'), JSON.stringify({
42
+ name: 'shared-testing',
43
+ tags: ['scope:shared', 'type:shared'],
44
+ }));
45
+ const stepFile = path.join(sharedDir, 'support', 'steps', 'web.step.ts');
46
+ const specFile = path.join(sharedDir, 'specs', 'TC-001', 'spec.md');
47
+ const actionFile = path.join(sharedDir, 'support', 'actions', 'web.action.ts');
48
+ expect(isInsideSharedLibrary(stepFile)).toBe(true);
49
+ expect(isInsideSharedLibrary(specFile)).toBe(true);
50
+ expect(isInsideSharedLibrary(actionFile)).toBe(true);
51
+ expect(isInsideSharedLibrary(sharedDir)).toBe(true);
52
+ });
53
+ it('returns true for shared library regardless of folder name if tagged in project.json', () => {
54
+ const customLibDir = path.join(tempDir, 'custom-helpers', 'common');
55
+ fs.mkdirSync(customLibDir, { recursive: true });
56
+ fs.writeFileSync(path.join(customLibDir, 'project.json'), JSON.stringify({
57
+ name: 'custom-common-lib',
58
+ tags: ['scope:shared', 'type:shared'],
59
+ }));
60
+ const actionFile = path.join(customLibDir, 'support', 'actions', 'click.action.ts');
61
+ expect(isInsideSharedLibrary(actionFile)).toBe(true);
62
+ });
63
+ it('returns false when folder is named "shared" but project.json is not tagged as shared', () => {
64
+ const sharedAppDir = path.join(tempDir, 'shared-app');
65
+ fs.mkdirSync(sharedAppDir, { recursive: true });
66
+ fs.writeFileSync(path.join(sharedAppDir, 'project.json'), JSON.stringify({
67
+ name: 'shared-app',
68
+ tags: ['scope:shared-app', 'type:e2e'],
69
+ }));
70
+ const specFile = path.join(sharedAppDir, 'specs', 'TC-001', 'spec.md');
71
+ expect(isInsideSharedLibrary(specFile)).toBe(false);
72
+ });
73
+ it('returns false for feature package project.json without shared tags', () => {
74
+ const featureDir = path.join(tempDir, 'packages', 'playground', 'e2e');
75
+ fs.mkdirSync(featureDir, { recursive: true });
76
+ fs.writeFileSync(path.join(featureDir, 'project.json'), JSON.stringify({
77
+ name: 'playground-e2e',
78
+ tags: ['scope:playground', 'type:e2e'],
79
+ }));
80
+ const specFile = path.join(featureDir, 'specs', 'TC-001', 'spec.md');
81
+ expect(isInsideSharedLibrary(specFile)).toBe(false);
82
+ });
83
+ it('returns false when no project.json exists in directory tree', () => {
84
+ const nonProjectSpec = path.join(tempDir, 'standalone', 'specs', 'TC-001', 'spec.md');
85
+ expect(isInsideSharedLibrary(nonProjectSpec)).toBe(false);
86
+ });
87
+ });
88
+ describe('testspectra/no-specs-in-shared-lib validation', () => {
89
+ it('emits testspectra/no-specs-in-shared-lib when spec.md is found inside shared/testing/', () => {
90
+ const sharedLibDir = path.join(tempDir, 'shared', 'testing');
91
+ fs.mkdirSync(sharedLibDir, { recursive: true });
92
+ fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
93
+ name: 'shared-testing',
94
+ tags: ['scope:shared', 'type:shared'],
95
+ }));
96
+ const sharedSpecDir = path.join(sharedLibDir, 'specs', 'TC-error');
97
+ fs.mkdirSync(sharedSpecDir, { recursive: true });
98
+ fs.writeFileSync(path.join(sharedSpecDir, 'spec.md'), `---
99
+ id: TC-ERROR-001
100
+ title: Spec inside shared lib
101
+ priority: High
102
+ caseType: Positive
103
+ status: draft
104
+ ---
105
+
106
+ # Spec inside shared lib
107
+
108
+ ## Test Steps
109
+ 1. Step one
110
+ `);
111
+ const result = SpecLinter.validateWorkspace(tempDir);
112
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
113
+ expect(errors).toHaveLength(1);
114
+ expect(errors[0].severity).toBe('error');
115
+ expect(errors[0].message).toContain('shared testing library');
116
+ });
117
+ it('emits testspectra/no-specs-in-shared-lib when suite.md is found inside shared/testing/', () => {
118
+ const sharedLibDir = path.join(tempDir, 'shared', 'testing');
119
+ fs.mkdirSync(sharedLibDir, { recursive: true });
120
+ fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
121
+ name: 'shared-testing',
122
+ tags: ['scope:shared', 'type:shared'],
123
+ }));
124
+ const sharedSuiteDir = path.join(sharedLibDir, 'support');
125
+ fs.mkdirSync(sharedSuiteDir, { recursive: true });
126
+ fs.writeFileSync(path.join(sharedSuiteDir, 'suite.md'), `---
127
+ id: suite-error
128
+ name: Suite inside shared lib
129
+ status: active
130
+ ---
131
+
132
+ # Suite: Inside Shared Lib
133
+ `);
134
+ const result = SpecLinter.validateWorkspace(tempDir);
135
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
136
+ expect(errors).toHaveLength(1);
137
+ expect(errors[0].message).toContain('shared testing library');
138
+ });
139
+ it('emits testspectra/no-specs-in-shared-lib when spec.md is inside a shared library with non-"shared" folder name', () => {
140
+ const customLibDir = path.join(tempDir, 'libs', 'my-testing-utils');
141
+ fs.mkdirSync(customLibDir, { recursive: true });
142
+ fs.writeFileSync(path.join(customLibDir, 'project.json'), JSON.stringify({
143
+ name: 'my-testing-utils',
144
+ tags: ['scope:shared', 'type:shared'],
145
+ }));
146
+ const specDir = path.join(customLibDir, 'specs', 'TC-custom');
147
+ fs.mkdirSync(specDir, { recursive: true });
148
+ fs.writeFileSync(path.join(specDir, 'spec.md'), `---
149
+ id: TC-CUSTOM-001
150
+ title: Spec inside custom shared lib
151
+ priority: High
152
+ caseType: Positive
153
+ status: draft
154
+ ---
155
+
156
+ # Spec inside custom shared lib
157
+
158
+ ## Test Steps
159
+ 1. Step one
160
+ `);
161
+ const result = SpecLinter.validateWorkspace(tempDir);
162
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
163
+ expect(errors).toHaveLength(1);
164
+ expect(errors[0].severity).toBe('error');
165
+ expect(errors[0].message).toContain('shared testing library');
166
+ });
167
+ it('does NOT emit testspectra/no-specs-in-shared-lib for specs in feature package directories', () => {
168
+ const featureDir = path.join(tempDir, 'packages', 'playground', 'e2e');
169
+ fs.mkdirSync(featureDir, { recursive: true });
170
+ fs.writeFileSync(path.join(featureDir, 'project.json'), JSON.stringify({
171
+ name: 'playground-e2e',
172
+ tags: ['scope:playground', 'type:e2e'],
173
+ }));
174
+ const featureSpecDir = path.join(featureDir, 'specs', 'TC-001');
175
+ fs.mkdirSync(featureSpecDir, { recursive: true });
176
+ fs.writeFileSync(path.join(featureSpecDir, 'spec.md'), `---
177
+ id: TC-001
178
+ title: Feature spec
179
+ priority: High
180
+ caseType: Positive
181
+ status: draft
182
+ ---
183
+
184
+ # Feature spec
185
+
186
+ ## Test Steps
187
+ 1. Step one
188
+ `);
189
+ const result = SpecLinter.validateWorkspace(tempDir);
190
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
191
+ expect(errors).toHaveLength(0);
192
+ });
193
+ it('does NOT emit testspectra/no-specs-in-shared-lib when folder is named "shared" but project.json has e2e tags', () => {
194
+ const sharedE2eDir = path.join(tempDir, 'shared-feature', 'e2e');
195
+ fs.mkdirSync(sharedE2eDir, { recursive: true });
196
+ fs.writeFileSync(path.join(sharedE2eDir, 'project.json'), JSON.stringify({
197
+ name: 'shared-feature-e2e',
198
+ tags: ['scope:shared-feature', 'type:e2e'],
199
+ }));
200
+ const specDir = path.join(sharedE2eDir, 'specs', 'TC-001');
201
+ fs.mkdirSync(specDir, { recursive: true });
202
+ fs.writeFileSync(path.join(specDir, 'spec.md'), `---
203
+ id: TC-001
204
+ title: Shared named feature spec
205
+ priority: High
206
+ caseType: Positive
207
+ status: draft
208
+ ---
209
+
210
+ # Shared named feature spec
211
+
212
+ ## Test Steps
213
+ 1. Step one
214
+ `);
215
+ const result = SpecLinter.validateWorkspace(tempDir);
216
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
217
+ expect(errors).toHaveLength(0);
218
+ });
219
+ it('does NOT emit testspectra/no-specs-in-shared-lib for specs in root specs/ directory', () => {
220
+ const rootSpecDir = path.join(tempDir, 'specs', 'TC-001');
221
+ fs.mkdirSync(rootSpecDir, { recursive: true });
222
+ fs.writeFileSync(path.join(rootSpecDir, 'spec.md'), `---
223
+ id: TC-001
224
+ title: Root spec
225
+ priority: High
226
+ caseType: Positive
227
+ status: draft
228
+ ---
229
+
230
+ # Root spec
231
+
232
+ ## Test Steps
233
+ 1. Step one
234
+ `);
235
+ const result = SpecLinter.validateWorkspace(tempDir);
236
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
237
+ expect(errors).toHaveLength(0);
238
+ });
239
+ it('emits multiple testspectra/no-specs-in-shared-lib when multiple specs found in shared library', () => {
240
+ const sharedLibDir = path.join(tempDir, 'shared', 'testing');
241
+ fs.mkdirSync(sharedLibDir, { recursive: true });
242
+ fs.writeFileSync(path.join(sharedLibDir, 'project.json'), JSON.stringify({
243
+ name: 'shared-testing',
244
+ tags: ['scope:shared', 'type:shared'],
245
+ }));
246
+ const sharedDir1 = path.join(sharedLibDir, 'specs', 'TC-001');
247
+ const sharedDir2 = path.join(sharedLibDir, 'specs', 'TC-002');
248
+ fs.mkdirSync(sharedDir1, { recursive: true });
249
+ fs.mkdirSync(sharedDir2, { recursive: true });
250
+ const specContent = `---
251
+ id: TC-TEST
252
+ title: Test
253
+ priority: High
254
+ caseType: Positive
255
+ status: draft
256
+ ---
257
+
258
+ # Test
259
+
260
+ ## Test Steps
261
+ 1. Step
262
+ `;
263
+ fs.writeFileSync(path.join(sharedDir1, 'spec.md'), specContent);
264
+ fs.writeFileSync(path.join(sharedDir2, 'spec.md'), specContent);
265
+ const result = SpecLinter.validateWorkspace(tempDir);
266
+ const errors = result.diagnostics.filter((d) => d.code === 'testspectra/no-specs-in-shared-lib');
267
+ expect(errors).toHaveLength(2);
268
+ });
269
+ });
270
+ });
@@ -0,0 +1 @@
1
+ export {};