@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
@@ -0,0 +1,630 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ZodError } from 'zod';
4
+ import { areTitlesAligned, extractDeclaredStepsFromSpec, extractModuleExports, extractSymbolsFromTestCode, } from './extractor.js';
5
+ import { isInsideSharedLibrary } from './discovery.js';
6
+ import { parseMarkdownFrontmatter } from './parser.js';
7
+ import { SpecFrontmatterSchema, validateSpecSections } from './schemas/spec.schema.js';
8
+ import { SuiteFrontmatterSchema, validateSuiteSections } from './schemas/suite.schema.js';
9
+ /**
10
+ * Resolves an import specifier against the importing file's directory and checks whether it
11
+ * reaches into an ambient-global-backed directory (Page/Component Objects, Steps, Fixtures) —
12
+ * the only imports still forbidden in `.tsx` component test files. Everything else (React,
13
+ * component source under test, npm packages, test-utils helpers) is allowed in `.tsx`, unlike
14
+ * the fully zero-import `.ts` E2E spec contract.
15
+ *
16
+ * See docs/v2/features/component-testing/react/mount-convention.md §"Static Imports Are Allowed
17
+ * Here" for the rationale.
18
+ */
19
+ function resolvesToAmbientGlobalDir(specifier, testFilePath) {
20
+ if (!specifier.startsWith('.'))
21
+ return false; // bare package specifiers (react, etc.) are fine
22
+ const resolved = path.normalize(path.join(path.dirname(testFilePath), specifier));
23
+ const segments = resolved.split(path.sep);
24
+ return (segments.includes('page-objects') ||
25
+ segments.includes('fixtures') ||
26
+ (segments.includes('support') && segments.includes('steps')));
27
+ }
28
+ export class SpecLinter {
29
+ /**
30
+ * Validates a single spec.md file.
31
+ */
32
+ static validateSpecFile(filePath, contentOverride, companionCodeOverrides) {
33
+ const diagnostics = [];
34
+ if (contentOverride === undefined && !fs.existsSync(filePath)) {
35
+ return {
36
+ diagnostics: [
37
+ {
38
+ code: 'testspectra/parse-error',
39
+ severity: 'error',
40
+ message: `File does not exist: ${filePath}`,
41
+ filePath,
42
+ line: 1,
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
48
+ const parsed = parseMarkdownFrontmatter(content, filePath);
49
+ diagnostics.push(...parsed.diagnostics);
50
+ if (!parsed.hasFrontmatter || !parsed.data) {
51
+ return { diagnostics };
52
+ }
53
+ const data = parsed.data;
54
+ const id = typeof data.id === 'string' ? data.id : undefined;
55
+ const idLine = parsed.keyLineMap.get('id') || 1;
56
+ try {
57
+ SpecFrontmatterSchema.parse(data);
58
+ }
59
+ catch (err) {
60
+ if (err instanceof ZodError) {
61
+ for (const issue of err.issues) {
62
+ const field = issue.path[0] ? String(issue.path[0]) : '';
63
+ const line = parsed.keyLineMap.get(field) || 1;
64
+ if (issue.code === 'invalid_enum_value') {
65
+ diagnostics.push({
66
+ code: 'testspectra/invalid-frontmatter-enum',
67
+ severity: 'error',
68
+ message: issue.message,
69
+ filePath,
70
+ line,
71
+ });
72
+ }
73
+ else if (issue.code === 'invalid_type' &&
74
+ (issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
75
+ diagnostics.push({
76
+ code: 'testspectra/missing-frontmatter',
77
+ severity: 'error',
78
+ message: issue.message,
79
+ filePath,
80
+ line: 1,
81
+ });
82
+ }
83
+ else if (['priority', 'caseType', 'status'].includes(field)) {
84
+ diagnostics.push({
85
+ code: 'testspectra/invalid-frontmatter-enum',
86
+ severity: 'error',
87
+ message: issue.message,
88
+ filePath,
89
+ line,
90
+ });
91
+ }
92
+ else {
93
+ diagnostics.push({
94
+ code: 'testspectra/missing-frontmatter',
95
+ severity: 'error',
96
+ message: issue.message,
97
+ filePath,
98
+ line,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ else {
104
+ diagnostics.push({
105
+ code: 'testspectra/parse-error',
106
+ severity: 'error',
107
+ message: err.message,
108
+ filePath,
109
+ line: 1,
110
+ });
111
+ }
112
+ }
113
+ // Validate markdown body sections
114
+ const sectionDiagnostics = validateSpecSections(parsed.body, filePath, parsed.bodyStartLine);
115
+ diagnostics.push(...sectionDiagnostics);
116
+ // ─────────────────────────────────────────────────────────────────────────
117
+ // Stage 3 & 4: AST Step Parity & Test Structure Enforcement
118
+ // ─────────────────────────────────────────────────────────────────────────
119
+ const declaredSteps = extractDeclaredStepsFromSpec(parsed.body, parsed.bodyStartLine);
120
+ const specDir = path.dirname(filePath);
121
+ let companionTestFiles = [];
122
+ try {
123
+ const entries = fs.readdirSync(specDir, { withFileTypes: true });
124
+ companionTestFiles = entries
125
+ .filter((e) => e.isFile() &&
126
+ (e.name.endsWith('.test.ts') ||
127
+ e.name.endsWith('.test.tsx') ||
128
+ e.name.endsWith('.web.ts') ||
129
+ e.name.endsWith('.web.tsx') ||
130
+ e.name.endsWith('.mobile.ts') ||
131
+ e.name.endsWith('.mobile.tsx') ||
132
+ e.name.endsWith('.android.ts') ||
133
+ e.name.endsWith('.android.tsx') ||
134
+ e.name.endsWith('.ios.ts') ||
135
+ e.name.endsWith('.ios.tsx') ||
136
+ e.name.endsWith('.common.ts') ||
137
+ e.name.endsWith('.common.tsx')))
138
+ .map((e) => path.join(specDir, e.name));
139
+ }
140
+ catch { }
141
+ if (companionCodeOverrides) {
142
+ const overrideKeys = companionCodeOverrides instanceof Map
143
+ ? Array.from(companionCodeOverrides.keys())
144
+ : Object.keys(companionCodeOverrides);
145
+ for (const overridePath of overrideKeys) {
146
+ if (path.dirname(overridePath) === specDir && !companionTestFiles.includes(overridePath)) {
147
+ companionTestFiles.push(overridePath);
148
+ }
149
+ }
150
+ }
151
+ if (companionTestFiles.length === 0) {
152
+ if (declaredSteps.length > 0) {
153
+ for (const step of declaredSteps) {
154
+ diagnostics.push({
155
+ code: 'testspectra/missing-step-call',
156
+ severity: 'error',
157
+ message: `Parity Error: Step "@step:${step.name}" declared in spec.md line ${step.line} is missing in test code (no companion test files found)`,
158
+ filePath,
159
+ line: step.line,
160
+ });
161
+ }
162
+ }
163
+ }
164
+ else {
165
+ const coverage = data.coverage;
166
+ for (const testFilePath of companionTestFiles) {
167
+ const testFileName = path.basename(testFilePath);
168
+ let testCode = '';
169
+ if (companionCodeOverrides) {
170
+ if (companionCodeOverrides instanceof Map) {
171
+ testCode = companionCodeOverrides.get(testFilePath) || '';
172
+ }
173
+ else {
174
+ testCode = companionCodeOverrides[testFilePath] || '';
175
+ }
176
+ }
177
+ if (!testCode) {
178
+ try {
179
+ testCode = fs.readFileSync(testFilePath, 'utf-8');
180
+ }
181
+ catch {
182
+ continue;
183
+ }
184
+ }
185
+ const symbols = extractSymbolsFromTestCode(testCode, testFilePath);
186
+ const isComponentTestFile = testFilePath.endsWith('.tsx');
187
+ // testspectra/no-manual-import: Manual import statement detected (must be zero-import).
188
+ // `.tsx` component test files (docs/v2/features/component-testing/react/) are exempt
189
+ // EXCEPT for imports that reach into ambient-global-backed directories (Page/Component
190
+ // Objects, Steps, Fixtures) — those stay forbidden so the ambient global is the single
191
+ // source of truth there too, exactly as in zero-import `.ts` E2E specs.
192
+ for (const imp of symbols.importStatements) {
193
+ if (isComponentTestFile && !resolvesToAmbientGlobalDir(imp.specifier, testFilePath)) {
194
+ continue;
195
+ }
196
+ diagnostics.push({
197
+ code: 'testspectra/no-manual-import',
198
+ severity: 'error',
199
+ message: isComponentTestFile
200
+ ? `Manual import of "${imp.specifier}" is forbidden; Page/Component Objects, Steps, and Fixtures are ambient globals even in component test files`
201
+ : 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
202
+ filePath: testFilePath,
203
+ line: imp.line,
204
+ column: imp.column,
205
+ });
206
+ }
207
+ // testspectra/no-test-block: forbidden test() calls detected
208
+ for (const tb of symbols.testBlocks) {
209
+ diagnostics.push({
210
+ code: 'testspectra/no-test-block',
211
+ severity: 'error',
212
+ message: 'test() is forbidden; TestSpectra test scripts must strictly use it() instead of test()',
213
+ filePath: testFilePath,
214
+ line: tb.line,
215
+ column: tb.column,
216
+ });
217
+ }
218
+ // testspectra/single-it-required: Multiple it() blocks (or 0 it blocks) detected in a single test case script
219
+ if (symbols.itBlocks.length === 0) {
220
+ if (symbols.testBlocks.length === 0) {
221
+ diagnostics.push({
222
+ code: 'testspectra/single-it-required',
223
+ severity: 'error',
224
+ message: 'Test case script must contain exactly one it() block (found 0)',
225
+ filePath: testFilePath,
226
+ line: 1,
227
+ });
228
+ }
229
+ }
230
+ else if (symbols.itBlocks.length > 1) {
231
+ for (let i = 1; i < symbols.itBlocks.length; i++) {
232
+ const extra = symbols.itBlocks[i];
233
+ diagnostics.push({
234
+ code: 'testspectra/single-it-required',
235
+ severity: 'error',
236
+ message: `Multiple it() blocks detected (${symbols.itBlocks.length}); each test script must contain exactly one it() block`,
237
+ filePath: testFilePath,
238
+ line: extra.line,
239
+ column: extra.column,
240
+ });
241
+ }
242
+ }
243
+ // testspectra/no-describe-block: describe(...) block detected (suites are strictly structured via directory hierarchy)
244
+ for (const desc of symbols.describeBlocks) {
245
+ diagnostics.push({
246
+ code: 'testspectra/no-describe-block',
247
+ severity: 'error',
248
+ message: 'describe() blocks are forbidden; test suites are structured via directory hierarchy with suite.md',
249
+ filePath: testFilePath,
250
+ line: desc.line,
251
+ column: desc.column,
252
+ });
253
+ }
254
+ // testspectra/title-mismatch: it("...") title in test script differs from title declared in spec.md frontmatter
255
+ if (symbols.itBlocks.length > 0 && typeof data.title === 'string') {
256
+ const itTitle = symbols.itBlocks[0].title;
257
+ if (!areTitlesAligned(data.title, itTitle)) {
258
+ diagnostics.push({
259
+ code: 'testspectra/title-mismatch',
260
+ severity: 'warning',
261
+ message: `Test title "${itTitle}" in ${testFileName}:L${symbols.itBlocks[0].line} does not match spec.md title "${data.title}"`,
262
+ filePath: testFilePath,
263
+ line: symbols.itBlocks[0].line,
264
+ column: symbols.itBlocks[0].column,
265
+ });
266
+ }
267
+ }
268
+ // Determine if platform coverage is explicitly marked manual or unsupported
269
+ let isPlatformAutomated = true;
270
+ if (coverage) {
271
+ if (testFileName.startsWith('web.') && (coverage.web === 'manual' || coverage.web === 'unsupported')) {
272
+ isPlatformAutomated = false;
273
+ }
274
+ else if ((testFileName.startsWith('mobile.') ||
275
+ testFileName.startsWith('android.') ||
276
+ testFileName.startsWith('ios.')) &&
277
+ (coverage.mobile === 'manual' || coverage.mobile === 'unsupported')) {
278
+ isPlatformAutomated = false;
279
+ }
280
+ }
281
+ const codeStepNames = new Set(symbols.stepCalls.map((s) => s.name));
282
+ // testspectra/missing-step-call: Step declared as @step:<name> in spec.md is absent in companion test script
283
+ if (isPlatformAutomated) {
284
+ for (const declaredStep of declaredSteps) {
285
+ if (!codeStepNames.has(declaredStep.name)) {
286
+ diagnostics.push({
287
+ code: 'testspectra/missing-step-call',
288
+ severity: 'error',
289
+ message: `Parity Error: Step "@step:${declaredStep.name}" declared in spec.md line ${declaredStep.line} is missing in ${testFileName}`,
290
+ filePath,
291
+ line: declaredStep.line,
292
+ });
293
+ }
294
+ }
295
+ }
296
+ // testspectra/undocumented-step: Step.<name>() called in test script is not documented with @step:<name> in spec.md
297
+ const declaredStepNames = new Set(declaredSteps.map((s) => s.name));
298
+ for (const stepCall of symbols.stepCalls) {
299
+ if (!declaredStepNames.has(stepCall.name)) {
300
+ diagnostics.push({
301
+ code: 'testspectra/undocumented-step',
302
+ severity: 'warning',
303
+ message: `Step "${stepCall.name}" called in ${testFileName}:L${stepCall.line} is not documented with @step:${stepCall.name} in spec.md`,
304
+ filePath: testFilePath,
305
+ line: stepCall.line,
306
+ column: stepCall.column,
307
+ });
308
+ }
309
+ }
310
+ }
311
+ }
312
+ return { diagnostics, id, idLine };
313
+ }
314
+ /**
315
+ * Validates a single suite.md file.
316
+ */
317
+ static validateSuiteFile(filePath, contentOverride) {
318
+ const diagnostics = [];
319
+ if (contentOverride === undefined && !fs.existsSync(filePath)) {
320
+ return {
321
+ diagnostics: [
322
+ {
323
+ code: 'testspectra/parse-error',
324
+ severity: 'error',
325
+ message: `File does not exist: ${filePath}`,
326
+ filePath,
327
+ line: 1,
328
+ },
329
+ ],
330
+ };
331
+ }
332
+ const content = contentOverride !== undefined ? contentOverride : fs.readFileSync(filePath, 'utf-8');
333
+ const parsed = parseMarkdownFrontmatter(content, filePath);
334
+ diagnostics.push(...parsed.diagnostics);
335
+ if (!parsed.hasFrontmatter || !parsed.data) {
336
+ return { diagnostics };
337
+ }
338
+ const data = parsed.data;
339
+ const id = typeof data.id === 'string' ? data.id : undefined;
340
+ const idLine = parsed.keyLineMap.get('id') || 1;
341
+ try {
342
+ SuiteFrontmatterSchema.parse(data);
343
+ }
344
+ catch (err) {
345
+ if (err instanceof ZodError) {
346
+ for (const issue of err.issues) {
347
+ const field = issue.path[0] ? String(issue.path[0]) : '';
348
+ const line = parsed.keyLineMap.get(field) || 1;
349
+ if (issue.code === 'invalid_enum_value') {
350
+ diagnostics.push({
351
+ code: 'testspectra/invalid-frontmatter-enum',
352
+ severity: 'error',
353
+ message: issue.message,
354
+ filePath,
355
+ line,
356
+ });
357
+ }
358
+ else if (issue.code === 'invalid_type' &&
359
+ (issue.received === 'undefined' || issue.message.toLowerCase().includes('missing required'))) {
360
+ diagnostics.push({
361
+ code: 'testspectra/missing-frontmatter',
362
+ severity: 'error',
363
+ message: issue.message,
364
+ filePath,
365
+ line: 1,
366
+ });
367
+ }
368
+ else if (field === 'status') {
369
+ diagnostics.push({
370
+ code: 'testspectra/invalid-frontmatter-enum',
371
+ severity: 'error',
372
+ message: issue.message,
373
+ filePath,
374
+ line,
375
+ });
376
+ }
377
+ else {
378
+ diagnostics.push({
379
+ code: 'testspectra/missing-frontmatter',
380
+ severity: 'error',
381
+ message: issue.message,
382
+ filePath,
383
+ line,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ else {
389
+ diagnostics.push({
390
+ code: 'testspectra/parse-error',
391
+ severity: 'error',
392
+ message: err.message,
393
+ filePath,
394
+ line: 1,
395
+ });
396
+ }
397
+ }
398
+ // Validate markdown body sections
399
+ const sectionDiagnostics = validateSuiteSections(parsed.body, filePath, parsed.bodyStartLine);
400
+ diagnostics.push(...sectionDiagnostics);
401
+ return { diagnostics, id, idLine };
402
+ }
403
+ /**
404
+ * Validates all spec.md and suite.md files in a workspace or specific target path.
405
+ */
406
+ static validateWorkspace(workspaceDir, targetPath) {
407
+ const rootScan = targetPath ? path.resolve(workspaceDir, targetPath) : workspaceDir;
408
+ const specFiles = [];
409
+ const suiteFiles = [];
410
+ // Collect files
411
+ if (fs.existsSync(rootScan)) {
412
+ const stat = fs.statSync(rootScan);
413
+ if (stat.isFile()) {
414
+ const base = path.basename(rootScan);
415
+ if (base === 'spec.md')
416
+ specFiles.push(rootScan);
417
+ else if (base === 'suite.md')
418
+ suiteFiles.push(rootScan);
419
+ }
420
+ else {
421
+ // Directory traversal
422
+ const traverse = (dir) => {
423
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
424
+ for (const entry of entries) {
425
+ const fullPath = path.join(dir, entry.name);
426
+ if (entry.isDirectory()) {
427
+ if (entry.name === 'node_modules' ||
428
+ entry.name === 'dist' ||
429
+ entry.name === '.git' ||
430
+ entry.name === '.testspectra') {
431
+ continue;
432
+ }
433
+ traverse(fullPath);
434
+ }
435
+ else if (entry.isFile()) {
436
+ if (entry.name === 'spec.md') {
437
+ specFiles.push(fullPath);
438
+ }
439
+ else if (entry.name === 'suite.md') {
440
+ suiteFiles.push(fullPath);
441
+ }
442
+ }
443
+ }
444
+ };
445
+ // If target not specified and `specs/` exists, scan `specs/` by default
446
+ const defaultSpecsDir = path.join(rootScan, 'specs');
447
+ if (!targetPath && fs.existsSync(defaultSpecsDir)) {
448
+ traverse(defaultSpecsDir);
449
+ }
450
+ else {
451
+ traverse(rootScan);
452
+ }
453
+ }
454
+ }
455
+ const allDiagnostics = [];
456
+ const idTracker = new Map();
457
+ let passedSpecs = 0;
458
+ let passedSuites = 0;
459
+ // Validate suites first
460
+ for (const suitePath of suiteFiles) {
461
+ const { diagnostics, id, idLine } = this.validateSuiteFile(suitePath);
462
+ let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
463
+ if (id) {
464
+ if (idTracker.has(id)) {
465
+ const prev = idTracker.get(id);
466
+ diagnostics.push({
467
+ code: 'testspectra/duplicate-spec-id',
468
+ severity: 'error',
469
+ message: `Duplicate suite id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
470
+ filePath: suitePath,
471
+ line: idLine || 1,
472
+ });
473
+ fileValid = false;
474
+ }
475
+ else {
476
+ idTracker.set(id, { filePath: suitePath, line: idLine || 1 });
477
+ }
478
+ }
479
+ if (fileValid) {
480
+ passedSuites++;
481
+ }
482
+ allDiagnostics.push(...diagnostics);
483
+ }
484
+ // Validate specs
485
+ for (const specPath of specFiles) {
486
+ const { diagnostics, id, idLine } = this.validateSpecFile(specPath);
487
+ let fileValid = diagnostics.filter((d) => d.severity === 'error').length === 0;
488
+ if (id) {
489
+ if (idTracker.has(id)) {
490
+ const prev = idTracker.get(id);
491
+ diagnostics.push({
492
+ code: 'testspectra/duplicate-spec-id',
493
+ severity: 'error',
494
+ message: `Duplicate test case id "${id}" detected across the workspace (first seen in ${path.relative(workspaceDir, prev.filePath)})`,
495
+ filePath: specPath,
496
+ line: idLine || 1,
497
+ });
498
+ fileValid = false;
499
+ }
500
+ else {
501
+ idTracker.set(id, { filePath: specPath, line: idLine || 1 });
502
+ }
503
+ }
504
+ if (fileValid) {
505
+ passedSpecs++;
506
+ }
507
+ allDiagnostics.push(...diagnostics);
508
+ }
509
+ // ─────────────────────────────────────────────────────────────────────────
510
+ // testspectra/no-specs-in-shared-lib: Specs or test scripts detected inside shared testing library
511
+ // ─────────────────────────────────────────────────────────────────────────
512
+ for (const specPath of specFiles) {
513
+ if (isInsideSharedLibrary(specPath)) {
514
+ allDiagnostics.push({
515
+ code: 'testspectra/no-specs-in-shared-lib',
516
+ severity: 'error',
517
+ message: `Test specification detected inside shared testing library; specs must be placed in feature package directories`,
518
+ filePath: specPath,
519
+ line: 1,
520
+ });
521
+ }
522
+ }
523
+ for (const suitePath of suiteFiles) {
524
+ if (isInsideSharedLibrary(suitePath)) {
525
+ allDiagnostics.push({
526
+ code: 'testspectra/no-specs-in-shared-lib',
527
+ severity: 'error',
528
+ message: `Suite specification detected inside shared testing library; suites must be placed in feature package directories`,
529
+ filePath: suitePath,
530
+ line: 1,
531
+ });
532
+ }
533
+ }
534
+ // ─────────────────────────────────────────────────────────────────────────
535
+ // testspectra/no-manual-import (extended) & testspectra/require-anonymous-default-export: Scan shared scripts workspace-wide
536
+ // ─────────────────────────────────────────────────────────────────────────
537
+ const sharedScriptExtensions = ['.step.ts', '.action.ts', '.hook.ts'];
538
+ const collectSharedScripts = (dir) => {
539
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
540
+ for (const entry of entries) {
541
+ const fullPath = path.join(dir, entry.name);
542
+ if (entry.isDirectory()) {
543
+ if (entry.name === 'node_modules' ||
544
+ entry.name === 'dist' ||
545
+ entry.name === '.git' ||
546
+ entry.name === '.testspectra') {
547
+ continue;
548
+ }
549
+ collectSharedScripts(fullPath);
550
+ }
551
+ else if (entry.isFile()) {
552
+ const isSharedScript = sharedScriptExtensions.some((ext) => entry.name.endsWith(ext));
553
+ if (!isSharedScript)
554
+ continue;
555
+ let content = '';
556
+ try {
557
+ content = fs.readFileSync(fullPath, 'utf-8');
558
+ }
559
+ catch {
560
+ continue;
561
+ }
562
+ // testspectra/no-manual-import: Check for manual imports in non-test scripts
563
+ const symbols = extractSymbolsFromTestCode(content, fullPath);
564
+ for (const imp of symbols.importStatements) {
565
+ allDiagnostics.push({
566
+ code: 'testspectra/no-manual-import',
567
+ severity: 'error',
568
+ message: 'Manual imports forbidden; all TestSpectra APIs, Page Objects, Steps, and Fixtures are ambient globals',
569
+ filePath: fullPath,
570
+ line: imp.line,
571
+ column: imp.column,
572
+ });
573
+ }
574
+ // testspectra/require-anonymous-default-export: Check for anonymous async default export
575
+ const moduleExports = extractModuleExports(content, fullPath);
576
+ // testspectra/require-anonymous-default-export: Named exports are forbidden
577
+ for (const ne of moduleExports.namedExportLines) {
578
+ allDiagnostics.push({
579
+ code: 'testspectra/require-anonymous-default-export',
580
+ severity: 'error',
581
+ message: `Named export "${ne.name}" is forbidden; shared modules must use anonymous default export only`,
582
+ filePath: fullPath,
583
+ line: ne.line,
584
+ column: ne.column,
585
+ });
586
+ }
587
+ if (!moduleExports.hasAnonymousAsyncDefaultExport && moduleExports.namedExportLines.length === 0) {
588
+ allDiagnostics.push({
589
+ code: 'testspectra/require-anonymous-default-export',
590
+ severity: 'error',
591
+ message: 'Shared step/action/hook must export an anonymous async function as default export',
592
+ filePath: fullPath,
593
+ line: 1,
594
+ });
595
+ }
596
+ // testspectra/require-anonymous-default-export: Test blocks are forbidden in shared modules
597
+ for (const tb of moduleExports.testBlocks) {
598
+ allDiagnostics.push({
599
+ code: 'testspectra/require-anonymous-default-export',
600
+ severity: 'error',
601
+ message: `${tb.name}() block is forbidden inside shared step/action/hook modules`,
602
+ filePath: fullPath,
603
+ line: tb.line,
604
+ column: tb.column,
605
+ });
606
+ }
607
+ }
608
+ }
609
+ };
610
+ try {
611
+ collectSharedScripts(rootScan);
612
+ }
613
+ catch { }
614
+ const errorCount = allDiagnostics.filter((d) => d.severity === 'error').length;
615
+ const warningCount = allDiagnostics.filter((d) => d.severity === 'warning').length;
616
+ const stats = {
617
+ totalSpecs: specFiles.length,
618
+ totalSuites: suiteFiles.length,
619
+ passedSpecs,
620
+ passedSuites,
621
+ errorCount,
622
+ warningCount,
623
+ };
624
+ return {
625
+ valid: errorCount === 0,
626
+ diagnostics: allDiagnostics,
627
+ stats,
628
+ };
629
+ }
630
+ }
@@ -0,0 +1,28 @@
1
+ export type DiagnosticSeverity = 'error' | 'warning';
2
+ export type LintDiagnosticCode = 'testspectra/parse-error' | 'testspectra/missing-frontmatter' | 'testspectra/invalid-frontmatter-enum' | 'testspectra/missing-step-call' | 'testspectra/undocumented-step' | 'testspectra/title-mismatch' | 'testspectra/no-manual-import' | 'testspectra/single-it-required' | 'testspectra/no-test-block' | 'testspectra/no-describe-block' | 'testspectra/duplicate-spec-id' | 'testspectra/no-specs-in-shared-lib' | 'testspectra/require-anonymous-default-export';
3
+ export interface LintDiagnostic {
4
+ code: LintDiagnosticCode;
5
+ severity: DiagnosticSeverity;
6
+ message: string;
7
+ filePath: string;
8
+ line: number;
9
+ column?: number;
10
+ }
11
+ export interface SpecLintStats {
12
+ totalSpecs: number;
13
+ totalSuites: number;
14
+ passedSpecs: number;
15
+ passedSuites: number;
16
+ errorCount: number;
17
+ warningCount: number;
18
+ }
19
+ export interface SpecLintResult {
20
+ valid: boolean;
21
+ diagnostics: LintDiagnostic[];
22
+ stats: SpecLintStats;
23
+ }
24
+ export interface LintOptions {
25
+ strict?: boolean;
26
+ json?: boolean;
27
+ cwd?: string;
28
+ }
@@ -0,0 +1 @@
1
+ export {};