@testspectra/cli 1.1.7 → 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,372 @@
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
+ describe('Step Parity & Symbol Integrity Linter (Stage 3 & Stage 4)', () => {
9
+ const tempDir = path.join(__dirname, '__temp_parity__');
10
+ beforeEach(() => {
11
+ fs.mkdirSync(tempDir, { recursive: true });
12
+ });
13
+ afterEach(() => {
14
+ if (fs.existsSync(tempDir)) {
15
+ fs.rmSync(tempDir, { recursive: true, force: true });
16
+ }
17
+ });
18
+ it('passes validation when declared @step directives match Step.*() invocations in web.test.ts', () => {
19
+ const specPath = path.join(tempDir, 'spec.md');
20
+ const testPath = path.join(tempDir, 'web.test.ts');
21
+ fs.writeFileSync(specPath, `---
22
+ id: TC-0100-matching
23
+ title: Valid login step matching
24
+ priority: High
25
+ caseType: Positive
26
+ status: ready-for-automation
27
+ coverage:
28
+ web: automated
29
+ ---
30
+
31
+ # Valid login step matching
32
+
33
+ ## Test Steps
34
+ 1. @step:login
35
+ 2. Assert dashboard loaded
36
+ `);
37
+ fs.writeFileSync(testPath, `it("Valid login step matching", async () => {
38
+ await Step.login();
39
+ await LoginPage.submitButton.should("be.visible");
40
+ });
41
+ `);
42
+ const result = SpecLinter.validateSpecFile(specPath);
43
+ expect(result.diagnostics).toHaveLength(0);
44
+ });
45
+ it('emits testspectra/missing-step-call when a declared @step is absent in web.test.ts', () => {
46
+ const specPath = path.join(tempDir, 'spec.md');
47
+ const testPath = path.join(tempDir, 'web.test.ts');
48
+ fs.writeFileSync(specPath, `---
49
+ id: TC-0101-missing-in-code
50
+ title: Step declared but missing in code
51
+ priority: High
52
+ caseType: Positive
53
+ status: ready-for-automation
54
+ coverage:
55
+ web: automated
56
+ ---
57
+
58
+ # Step declared but missing in code
59
+
60
+ ## Test Steps
61
+ 1. @step:login
62
+ 2. @step:logout
63
+ `);
64
+ fs.writeFileSync(testPath, `it("Step declared but missing in code", async () => {
65
+ await Step.login();
66
+ });
67
+ `);
68
+ const result = SpecLinter.validateSpecFile(specPath);
69
+ const missingErrors = result.diagnostics.filter((d) => d.code === 'testspectra/missing-step-call');
70
+ expect(missingErrors).toHaveLength(1);
71
+ expect(missingErrors[0].severity).toBe('error');
72
+ expect(missingErrors[0].message).toContain('Step "@step:logout" declared in spec.md line 15 is missing in web.test.ts');
73
+ expect(missingErrors[0].line).toBe(15);
74
+ });
75
+ it('emits testspectra/undocumented-step warning when Step.*() called in test script is not documented in spec.md', () => {
76
+ const specPath = path.join(tempDir, 'spec.md');
77
+ const testPath = path.join(tempDir, 'web.test.ts');
78
+ fs.writeFileSync(specPath, `---
79
+ id: TC-0102-undocumented-step
80
+ title: Undocumented step call
81
+ priority: Medium
82
+ caseType: Positive
83
+ status: ready-for-automation
84
+ coverage:
85
+ web: automated
86
+ ---
87
+
88
+ # Undocumented step call
89
+
90
+ ## Test Steps
91
+ 1. @step:login
92
+ `);
93
+ fs.writeFileSync(testPath, `it("Undocumented step call", async () => {
94
+ await Step.login();
95
+ await Step.logout();
96
+ });
97
+ `);
98
+ const result = SpecLinter.validateSpecFile(specPath);
99
+ const warnings = result.diagnostics.filter((d) => d.code === 'testspectra/undocumented-step');
100
+ expect(warnings).toHaveLength(1);
101
+ expect(warnings[0].severity).toBe('warning');
102
+ expect(warnings[0].message).toContain('Step "logout" called in web.test.ts:L3 is not documented with @step:logout in spec.md');
103
+ expect(warnings[0].line).toBe(3);
104
+ });
105
+ it('emits testspectra/title-mismatch warning when it() title in test script differs from spec.md title', () => {
106
+ const specPath = path.join(tempDir, 'spec.md');
107
+ const testPath = path.join(tempDir, 'web.test.ts');
108
+ fs.writeFileSync(specPath, `---
109
+ id: TC-0103-title-mismatch
110
+ title: Verify user profile updates
111
+ priority: High
112
+ caseType: Positive
113
+ status: ready-for-automation
114
+ coverage:
115
+ web: automated
116
+ ---
117
+
118
+ # Verify user profile updates
119
+
120
+ ## Test Steps
121
+ 1. Open profile and change name
122
+ `);
123
+ fs.writeFileSync(testPath, `it("should process checkout cart items", async () => {
124
+ await CartPage.checkout();
125
+ });
126
+ `);
127
+ const result = SpecLinter.validateSpecFile(specPath);
128
+ const titleWarnings = result.diagnostics.filter((d) => d.code === 'testspectra/title-mismatch');
129
+ expect(titleWarnings).toHaveLength(1);
130
+ expect(titleWarnings[0].severity).toBe('warning');
131
+ expect(titleWarnings[0].message).toContain('Test title "should process checkout cart items" in web.test.ts:L1 does not match spec.md title "Verify user profile updates"');
132
+ });
133
+ it('does not emit testspectra/title-mismatch warning when it() title is directly aligned with spec.md title', () => {
134
+ const specPath = path.join(tempDir, 'spec.md');
135
+ const testPath = path.join(tempDir, 'web.test.ts');
136
+ fs.writeFileSync(specPath, `---
137
+ id: TC-0104-title-aligned
138
+ title: Verify playground form inputs and default states
139
+ priority: High
140
+ caseType: Positive
141
+ status: ready-for-automation
142
+ coverage:
143
+ web: automated
144
+ ---
145
+
146
+ # Verify playground form inputs and default states
147
+
148
+ ## Test Steps
149
+ 1. Check form inputs
150
+ `);
151
+ fs.writeFileSync(testPath, `it("Verify playground form inputs and default states", async () => {
152
+ await PlaygroundPage.open();
153
+ });
154
+ `);
155
+ const result = SpecLinter.validateSpecFile(specPath);
156
+ const titleWarnings = result.diagnostics.filter((d) => d.code === 'testspectra/title-mismatch');
157
+ expect(titleWarnings).toHaveLength(0);
158
+ });
159
+ it('emits testspectra/no-manual-import error when manual import statements are detected in test script', () => {
160
+ const specPath = path.join(tempDir, 'spec.md');
161
+ const testPath = path.join(tempDir, 'web.test.ts');
162
+ fs.writeFileSync(specPath, `---
163
+ id: TC-0105-manual-imports
164
+ title: Forbidden manual imports
165
+ priority: High
166
+ caseType: Positive
167
+ status: ready-for-automation
168
+ coverage:
169
+ web: automated
170
+ ---
171
+
172
+ # Forbidden manual imports
173
+
174
+ ## Test Steps
175
+ 1. Test without manual imports
176
+ `);
177
+ fs.writeFileSync(testPath, `import { expect } from 'vitest';
178
+ import path from 'path';
179
+
180
+ it("Forbidden manual imports", async () => {
181
+ expect(1).toBe(1);
182
+ });
183
+ `);
184
+ const result = SpecLinter.validateSpecFile(specPath);
185
+ const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
186
+ expect(importErrors).toHaveLength(2);
187
+ expect(importErrors[0].severity).toBe('error');
188
+ expect(importErrors[0].message).toContain('Manual imports forbidden');
189
+ expect(importErrors[0].line).toBe(1);
190
+ expect(importErrors[1].line).toBe(2);
191
+ });
192
+ it('emits testspectra/single-it-required error when test script contains 0 it() blocks', () => {
193
+ const specPath = path.join(tempDir, 'spec.md');
194
+ const testPath = path.join(tempDir, 'web.test.ts');
195
+ fs.writeFileSync(specPath, `---
196
+ id: TC-0106-zero-it
197
+ title: Zero it blocks test
198
+ priority: Medium
199
+ caseType: Positive
200
+ status: ready-for-automation
201
+ coverage:
202
+ web: automated
203
+ ---
204
+
205
+ # Zero it blocks test
206
+
207
+ ## Test Steps
208
+ 1. Execute step
209
+ `);
210
+ fs.writeFileSync(testPath, `// Helper file with no test execution block
211
+ const dummy = 123;
212
+ `);
213
+ const result = SpecLinter.validateSpecFile(specPath);
214
+ const itErrors = result.diagnostics.filter((d) => d.code === 'testspectra/single-it-required');
215
+ expect(itErrors).toHaveLength(1);
216
+ expect(itErrors[0].severity).toBe('error');
217
+ expect(itErrors[0].message).toContain('Test case script must contain exactly one it() block (found 0)');
218
+ });
219
+ it('emits testspectra/single-it-required error when test script contains multiple it() blocks', () => {
220
+ const specPath = path.join(tempDir, 'spec.md');
221
+ const testPath = path.join(tempDir, 'web.test.ts');
222
+ fs.writeFileSync(specPath, `---
223
+ id: TC-0107-multiple-it
224
+ title: Multiple it blocks test
225
+ priority: Medium
226
+ caseType: Positive
227
+ status: ready-for-automation
228
+ coverage:
229
+ web: automated
230
+ ---
231
+
232
+ # Multiple it blocks test
233
+
234
+ ## Test Steps
235
+ 1. First step
236
+ 2. Second step
237
+ `);
238
+ fs.writeFileSync(testPath, `it("Multiple it blocks test", async () => {
239
+ await Step.first();
240
+ });
241
+
242
+ it("should execute second step", async () => {
243
+ await Step.second();
244
+ });
245
+ `);
246
+ const result = SpecLinter.validateSpecFile(specPath);
247
+ const itErrors = result.diagnostics.filter((d) => d.code === 'testspectra/single-it-required');
248
+ expect(itErrors).toHaveLength(1);
249
+ expect(itErrors[0].severity).toBe('error');
250
+ expect(itErrors[0].message).toContain('Multiple it() blocks detected (2); each test script must contain exactly one it() block');
251
+ expect(itErrors[0].line).toBe(5);
252
+ });
253
+ it('emits testspectra/no-test-block error when test() block is used instead of it()', () => {
254
+ const specPath = path.join(tempDir, 'spec.md');
255
+ const testPath = path.join(tempDir, 'web.test.ts');
256
+ fs.writeFileSync(specPath, `---
257
+ id: TC-0108-test-keyword
258
+ title: Test keyword forbidden
259
+ priority: Medium
260
+ caseType: Positive
261
+ status: ready-for-automation
262
+ coverage:
263
+ web: automated
264
+ ---
265
+
266
+ # Test keyword forbidden
267
+
268
+ ## Test Steps
269
+ 1. Execute step
270
+ `);
271
+ fs.writeFileSync(testPath, `test("Test keyword forbidden", async () => {
272
+ await Step.first();
273
+ });
274
+ `);
275
+ const result = SpecLinter.validateSpecFile(specPath);
276
+ const testErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-test-block');
277
+ expect(testErrors).toHaveLength(1);
278
+ expect(testErrors[0].severity).toBe('error');
279
+ expect(testErrors[0].message).toContain('test() is forbidden; TestSpectra test scripts must strictly use it() instead of test()');
280
+ expect(testErrors[0].line).toBe(1);
281
+ });
282
+ it('emits testspectra/no-describe-block error when describe() block is detected in test script', () => {
283
+ const specPath = path.join(tempDir, 'spec.md');
284
+ const testPath = path.join(tempDir, 'web.test.ts');
285
+ fs.writeFileSync(specPath, `---
286
+ id: TC-0108-describe-block
287
+ title: Describe block forbidden
288
+ priority: High
289
+ caseType: Positive
290
+ status: ready-for-automation
291
+ coverage:
292
+ web: automated
293
+ ---
294
+
295
+ # Describe block forbidden
296
+
297
+ ## Test Steps
298
+ 1. Execute test inside describe
299
+ `);
300
+ fs.writeFileSync(testPath, `describe("Describe block forbidden suite", () => {
301
+ it("Describe block forbidden", async () => {
302
+ await LoginPage.open();
303
+ });
304
+ });
305
+ `);
306
+ const result = SpecLinter.validateSpecFile(specPath);
307
+ const descErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-describe-block');
308
+ expect(descErrors).toHaveLength(1);
309
+ expect(descErrors[0].severity).toBe('error');
310
+ expect(descErrors[0].message).toContain('describe() blocks are forbidden; test suites are structured via directory hierarchy with suite.md');
311
+ expect(descErrors[0].line).toBe(1);
312
+ });
313
+ describe('testspectra/no-manual-import extended to non-test scripts', () => {
314
+ it('emits testspectra/no-manual-import when shared step file contains import statements', () => {
315
+ const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'login');
316
+ fs.mkdirSync(stepsDir, { recursive: true });
317
+ fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `import { expect } from 'vitest';
318
+ import path from 'path';
319
+
320
+ export default async function login() {
321
+ await LoginPage.open();
322
+ }
323
+ `);
324
+ const result = SpecLinter.validateWorkspace(tempDir);
325
+ const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
326
+ expect(importErrors).toHaveLength(2);
327
+ expect(importErrors[0].severity).toBe('error');
328
+ expect(importErrors[0].message).toContain('Manual imports forbidden');
329
+ expect(importErrors[0].filePath).toContain('web.step.ts');
330
+ });
331
+ it('emits testspectra/no-manual-import when shared action file contains import statements', () => {
332
+ const actionsDir = path.join(tempDir, 'shared', 'testing', 'support', 'actions', 'fillForm');
333
+ fs.mkdirSync(actionsDir, { recursive: true });
334
+ fs.writeFileSync(path.join(actionsDir, 'web.action.ts'), `import { Page } from 'playwright';
335
+
336
+ export default async function (email: string) {
337
+ await this.get("#email").type(email);
338
+ }
339
+ `);
340
+ const result = SpecLinter.validateWorkspace(tempDir);
341
+ const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
342
+ expect(importErrors).toHaveLength(1);
343
+ expect(importErrors[0].filePath).toContain('web.action.ts');
344
+ });
345
+ it('emits testspectra/no-manual-import when shared hook file contains import statements', () => {
346
+ const hooksDir = path.join(tempDir, 'shared', 'testing', 'global-hooks', 'before');
347
+ fs.mkdirSync(hooksDir, { recursive: true });
348
+ fs.writeFileSync(path.join(hooksDir, 'web.hook.ts'), `import { database } from './setup';
349
+
350
+ export default async function () {
351
+ await database.reset();
352
+ }
353
+ `);
354
+ const result = SpecLinter.validateWorkspace(tempDir);
355
+ const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
356
+ expect(importErrors).toHaveLength(1);
357
+ expect(importErrors[0].filePath).toContain('web.hook.ts');
358
+ });
359
+ it('does NOT emit testspectra/no-manual-import when shared step has no imports', () => {
360
+ const stepsDir = path.join(tempDir, 'shared', 'testing', 'support', 'steps', 'verify');
361
+ fs.mkdirSync(stepsDir, { recursive: true });
362
+ fs.writeFileSync(path.join(stepsDir, 'web.step.ts'), `export default async function verifyState() {
363
+ await Page.open();
364
+ await Page.element.should("be.visible");
365
+ }
366
+ `);
367
+ const result = SpecLinter.validateWorkspace(tempDir);
368
+ const importErrors = result.diagnostics.filter((d) => d.code === 'testspectra/no-manual-import');
369
+ expect(importErrors).toHaveLength(0);
370
+ });
371
+ });
372
+ });
@@ -0,0 +1,30 @@
1
+ export interface WorkspaceAssets {
2
+ sharedSteps: Set<string>;
3
+ pageObjects: Set<string>;
4
+ fixtures: Set<string>;
5
+ }
6
+ /**
7
+ * Scans a workspace directory for all `project.json` files that have both
8
+ * "scope:shared" and "type:shared" tags, and returns their parent folder paths.
9
+ */
10
+ export declare function findSharedLibraryRoots(workspaceDir: string): string[];
11
+ /**
12
+ * Resolves the root directory of a shared testing library if the file path is inside one.
13
+ * Determines shared library status based on `project.json` containing
14
+ * both "scope:shared" and "type:shared" tags in ancestor directories,
15
+ * taking the parent folder of that `project.json` as the shared library root.
16
+ */
17
+ export declare function getSharedLibraryRoot(filePath: string, workspaceDir?: string): string | null;
18
+ /**
19
+ * Checks if a file path is inside a shared testing library directory.
20
+ */
21
+ export declare function isInsideSharedLibrary(filePath: string, workspaceDir?: string): boolean;
22
+ /**
23
+ * Traverses upwards to locate the project root containing spectra.config.ts or package.json.
24
+ */
25
+ export declare function findProjectRoot(startDir: string): string;
26
+ /**
27
+ * Discovers and indexes all available shared steps, page objects, and fixtures
28
+ * across the workspace (including root and shared directories).
29
+ */
30
+ export declare function discoverWorkspaceAssets(workspaceDir: string): WorkspaceAssets;
@@ -0,0 +1,249 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ /**
4
+ * Scans a workspace directory for all `project.json` files that have both
5
+ * "scope:shared" and "type:shared" tags, and returns their parent folder paths.
6
+ */
7
+ export function findSharedLibraryRoots(workspaceDir) {
8
+ const sharedRoots = [];
9
+ const ignoredDirs = new Set(['node_modules', 'dist', '.git', '.testspectra', 'build', 'coverage']);
10
+ const scan = (dir) => {
11
+ if (!fs.existsSync(dir))
12
+ return;
13
+ try {
14
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
15
+ for (const entry of entries) {
16
+ if (entry.isDirectory()) {
17
+ if (ignoredDirs.has(entry.name))
18
+ continue;
19
+ scan(path.join(dir, entry.name));
20
+ }
21
+ else if (entry.isFile() && entry.name === 'project.json') {
22
+ try {
23
+ const raw = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
24
+ const parsed = JSON.parse(raw);
25
+ const tags = Array.isArray(parsed.tags) ? parsed.tags : [];
26
+ if (tags.includes('scope:shared') && tags.includes('type:shared')) {
27
+ sharedRoots.push(dir);
28
+ }
29
+ }
30
+ catch { }
31
+ }
32
+ }
33
+ }
34
+ catch { }
35
+ };
36
+ scan(workspaceDir);
37
+ return sharedRoots;
38
+ }
39
+ /**
40
+ * Resolves the root directory of a shared testing library if the file path is inside one.
41
+ * Determines shared library status based on `project.json` containing
42
+ * both "scope:shared" and "type:shared" tags in ancestor directories,
43
+ * taking the parent folder of that `project.json` as the shared library root.
44
+ */
45
+ export function getSharedLibraryRoot(filePath, workspaceDir) {
46
+ const resolved = workspaceDir && !path.isAbsolute(filePath) ? path.resolve(workspaceDir, filePath) : path.resolve(filePath);
47
+ let current = resolved;
48
+ try {
49
+ if (fs.existsSync(current)) {
50
+ if (fs.statSync(current).isFile()) {
51
+ current = path.dirname(current);
52
+ }
53
+ }
54
+ else if (path.extname(current)) {
55
+ current = path.dirname(current);
56
+ }
57
+ }
58
+ catch {
59
+ if (path.extname(current)) {
60
+ current = path.dirname(current);
61
+ }
62
+ }
63
+ while (true) {
64
+ const projectJsonPath = path.join(current, 'project.json');
65
+ if (fs.existsSync(projectJsonPath)) {
66
+ try {
67
+ const raw = fs.readFileSync(projectJsonPath, 'utf-8');
68
+ const projectConfig = JSON.parse(raw);
69
+ const tags = Array.isArray(projectConfig.tags) ? projectConfig.tags : [];
70
+ if (tags.includes('scope:shared') && tags.includes('type:shared')) {
71
+ return current;
72
+ }
73
+ return null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ const parent = path.dirname(current);
80
+ if (parent === current) {
81
+ break;
82
+ }
83
+ current = parent;
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Checks if a file path is inside a shared testing library directory.
89
+ */
90
+ export function isInsideSharedLibrary(filePath, workspaceDir) {
91
+ return getSharedLibraryRoot(filePath, workspaceDir) !== null;
92
+ }
93
+ /**
94
+ * Traverses upwards to locate the project root containing spectra.config.ts or package.json.
95
+ */
96
+ export function findProjectRoot(startDir) {
97
+ let current = path.resolve(startDir);
98
+ while (true) {
99
+ if (fs.existsSync(path.join(current, 'spectra.config.ts')) ||
100
+ fs.existsSync(path.join(current, 'spectra.config.js'))) {
101
+ return current;
102
+ }
103
+ const parent = path.dirname(current);
104
+ if (parent === current) {
105
+ break;
106
+ }
107
+ current = parent;
108
+ }
109
+ // Fallback: search for package.json
110
+ current = path.resolve(startDir);
111
+ while (true) {
112
+ if (fs.existsSync(path.join(current, 'package.json'))) {
113
+ return current;
114
+ }
115
+ const parent = path.dirname(current);
116
+ if (parent === current) {
117
+ break;
118
+ }
119
+ current = parent;
120
+ }
121
+ return startDir;
122
+ }
123
+ /**
124
+ * Discovers and indexes all available shared steps, page objects, and fixtures
125
+ * across the workspace (including root and shared directories).
126
+ */
127
+ export function discoverWorkspaceAssets(workspaceDir) {
128
+ const assets = {
129
+ sharedSteps: new Set(),
130
+ pageObjects: new Set(),
131
+ fixtures: new Set(),
132
+ };
133
+ const candidateRoots = [
134
+ workspaceDir,
135
+ ...findSharedLibraryRoots(workspaceDir),
136
+ path.join(workspaceDir, 'shared', 'testing'),
137
+ path.join(workspaceDir, 'shared'),
138
+ path.join(workspaceDir, 'testing'),
139
+ ];
140
+ // Also check packages in monorepos if packages/ directory exists
141
+ const packagesDir = path.join(workspaceDir, 'packages');
142
+ if (fs.existsSync(packagesDir)) {
143
+ try {
144
+ const pkgs = fs.readdirSync(packagesDir, { withFileTypes: true });
145
+ for (const pkg of pkgs) {
146
+ if (pkg.isDirectory()) {
147
+ candidateRoots.push(path.join(packagesDir, pkg.name));
148
+ candidateRoots.push(path.join(packagesDir, pkg.name, 'e2e'));
149
+ }
150
+ }
151
+ }
152
+ catch { }
153
+ }
154
+ const scanFileForPageClasses = (filePath) => {
155
+ try {
156
+ const content = fs.readFileSync(filePath, 'utf-8');
157
+ const matches = content.matchAll(/(?:class|interface)\s+([A-Za-z0-9_]+Page)\b/g);
158
+ for (const match of matches) {
159
+ assets.pageObjects.add(match[1]);
160
+ }
161
+ }
162
+ catch { }
163
+ };
164
+ for (const root of candidateRoots) {
165
+ if (!fs.existsSync(root))
166
+ continue;
167
+ // 1. Shared Steps (support/steps or steps)
168
+ const stepDirs = [path.join(root, 'support', 'steps'), path.join(root, 'steps')];
169
+ for (const sDir of stepDirs) {
170
+ if (fs.existsSync(sDir) && fs.statSync(sDir).isDirectory()) {
171
+ try {
172
+ const entries = fs.readdirSync(sDir, { withFileTypes: true });
173
+ for (const entry of entries) {
174
+ if (entry.isDirectory()) {
175
+ assets.sharedSteps.add(entry.name);
176
+ }
177
+ else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
178
+ const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
179
+ if (nameWithoutExt !== 'index') {
180
+ assets.sharedSteps.add(nameWithoutExt);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ catch { }
186
+ }
187
+ }
188
+ // 2. Page Objects (page-objects or pageobjects)
189
+ const poDirs = [path.join(root, 'page-objects'), path.join(root, 'pageobjects')];
190
+ for (const poDir of poDirs) {
191
+ if (fs.existsSync(poDir) && fs.statSync(poDir).isDirectory()) {
192
+ try {
193
+ const entries = fs.readdirSync(poDir, { withFileTypes: true });
194
+ for (const entry of entries) {
195
+ if (entry.isDirectory()) {
196
+ assets.pageObjects.add(entry.name);
197
+ // Also inspect files in the subdirectory
198
+ try {
199
+ const subEntries = fs.readdirSync(path.join(poDir, entry.name), { withFileTypes: true });
200
+ for (const sub of subEntries) {
201
+ if (sub.isFile() && (sub.name.endsWith('.ts') || sub.name.endsWith('.js'))) {
202
+ scanFileForPageClasses(path.join(poDir, entry.name, sub.name));
203
+ }
204
+ }
205
+ }
206
+ catch { }
207
+ }
208
+ else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
209
+ const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
210
+ if (nameWithoutExt !== 'index') {
211
+ assets.pageObjects.add(nameWithoutExt);
212
+ }
213
+ scanFileForPageClasses(path.join(poDir, entry.name));
214
+ }
215
+ }
216
+ }
217
+ catch { }
218
+ }
219
+ }
220
+ // 3. Fixtures (fixtures)
221
+ const fixDir = path.join(root, 'fixtures');
222
+ if (fs.existsSync(fixDir) && fs.statSync(fixDir).isDirectory()) {
223
+ try {
224
+ const entries = fs.readdirSync(fixDir, { withFileTypes: true });
225
+ for (const entry of entries) {
226
+ if (entry.isFile()) {
227
+ const nameWithoutExt = path.basename(entry.name, path.extname(entry.name));
228
+ assets.fixtures.add(nameWithoutExt);
229
+ }
230
+ else if (entry.isDirectory()) {
231
+ assets.fixtures.add(entry.name);
232
+ try {
233
+ const subEntries = fs.readdirSync(path.join(fixDir, entry.name), { withFileTypes: true });
234
+ for (const sub of subEntries) {
235
+ if (sub.isFile()) {
236
+ const subNameWithoutExt = path.basename(sub.name, path.extname(sub.name));
237
+ assets.fixtures.add(subNameWithoutExt);
238
+ }
239
+ }
240
+ }
241
+ catch { }
242
+ }
243
+ }
244
+ }
245
+ catch { }
246
+ }
247
+ }
248
+ return assets;
249
+ }