@avi770/testteam 1.2.0

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 (325) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/agents/01-analyst.ts +100 -0
  5. package/agents/02-seed-architect.ts +59 -0
  6. package/agents/03-test-generator.ts +191 -0
  7. package/agents/04-unit-runner.ts +160 -0
  8. package/agents/05-browser-crawler.ts +790 -0
  9. package/agents/06-api-exerciser.ts +311 -0
  10. package/agents/07-security-scout.ts +188 -0
  11. package/agents/08-a11y-guardian.ts +212 -0
  12. package/agents/09-healer.ts +228 -0
  13. package/agents/10-reporter.ts +266 -0
  14. package/agents/11-fixer.ts +253 -0
  15. package/agents/12-ux-inspector.ts +444 -0
  16. package/agents/13-performance-profiler.ts +271 -0
  17. package/agents/14-data-integrity-auditor.ts +417 -0
  18. package/agents/15-regression-sentinel.ts +307 -0
  19. package/agents/16-chaos-agent.ts +228 -0
  20. package/agents/17-documentation-validator.ts +266 -0
  21. package/agents/18-integration-watchdog.ts +178 -0
  22. package/agents/19-tenant-isolation-auditor.ts +199 -0
  23. package/agents/20-workflow-completion-tester.ts +203 -0
  24. package/agents/21-state-session-tester.ts +262 -0
  25. package/agents/22-email-notification-verifier.ts +244 -0
  26. package/agents/23-migration-tester.ts +80 -0
  27. package/agents/__tests__/01-analyst.test.ts +188 -0
  28. package/agents/__tests__/02-seed-architect.test.ts +152 -0
  29. package/agents/__tests__/03-test-generator-full.test.ts +321 -0
  30. package/agents/__tests__/03-test-generator.test.ts +318 -0
  31. package/agents/__tests__/04-unit-runner.test.ts +320 -0
  32. package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
  33. package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
  34. package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
  35. package/agents/__tests__/05-browser-crawler.test.ts +518 -0
  36. package/agents/__tests__/06-api-exerciser.test.ts +619 -0
  37. package/agents/__tests__/07-security-scout.test.ts +382 -0
  38. package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
  39. package/agents/__tests__/09-healer.test.ts +384 -0
  40. package/agents/__tests__/10-reporter.test.ts +366 -0
  41. package/agents/__tests__/11-fixer.test.ts +406 -0
  42. package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
  43. package/agents/__tests__/12-ux-inspector.test.ts +443 -0
  44. package/agents/__tests__/13-performance-profiler.test.ts +411 -0
  45. package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
  46. package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
  47. package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
  48. package/agents/__tests__/16-chaos-agent.test.ts +427 -0
  49. package/agents/__tests__/17-documentation-validator.test.ts +402 -0
  50. package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
  51. package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
  52. package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
  53. package/agents/__tests__/21-state-session-tester.test.ts +374 -0
  54. package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
  55. package/agents/__tests__/23-migration-tester.test.ts +145 -0
  56. package/agents/__tests__/base-agent.test.ts +188 -0
  57. package/agents/__tests__/registry.test.ts +218 -0
  58. package/agents/base-agent.ts +77 -0
  59. package/agents/registry.ts +136 -0
  60. package/baselines/api-schemas/.gitkeep +0 -0
  61. package/baselines/performance/.gitkeep +0 -0
  62. package/baselines/screenshots/.gitkeep +0 -0
  63. package/bin/testteam.js +10 -0
  64. package/core/__tests__/ci-output.test.ts +430 -0
  65. package/core/__tests__/cli.test.ts +387 -0
  66. package/core/__tests__/config.test.ts +78 -0
  67. package/core/__tests__/cost-tracker.test.ts +158 -0
  68. package/core/__tests__/evidence.test.ts +265 -0
  69. package/core/__tests__/fix-loop.test.ts +210 -0
  70. package/core/__tests__/health-check.test.ts +44 -0
  71. package/core/__tests__/init.test.ts +609 -0
  72. package/core/__tests__/integration.test.ts +204 -0
  73. package/core/__tests__/license-gen.test.ts +227 -0
  74. package/core/__tests__/license.test.ts +326 -0
  75. package/core/__tests__/multi-browser.test.ts +278 -0
  76. package/core/__tests__/orchestrator.test.ts +519 -0
  77. package/core/__tests__/phase-gate.test.ts +43 -0
  78. package/core/__tests__/report-html.test.ts +398 -0
  79. package/core/__tests__/report-upload.test.ts +325 -0
  80. package/core/__tests__/run-counter.test.ts +234 -0
  81. package/core/ci-output.ts +240 -0
  82. package/core/cli.ts +232 -0
  83. package/core/config.ts +178 -0
  84. package/core/cost-tracker.ts +59 -0
  85. package/core/evidence.ts +132 -0
  86. package/core/fix-loop.ts +85 -0
  87. package/core/health-check.ts +54 -0
  88. package/core/init.ts +546 -0
  89. package/core/license-gen.ts +212 -0
  90. package/core/license.ts +211 -0
  91. package/core/messages.ts +67 -0
  92. package/core/multi-browser.ts +136 -0
  93. package/core/orchestrator.ts +354 -0
  94. package/core/phase-gate.ts +55 -0
  95. package/core/report-html.ts +657 -0
  96. package/core/report-upload.ts +188 -0
  97. package/core/run-counter.ts +175 -0
  98. package/core/types.ts +57 -0
  99. package/dist/agents/01-analyst.d.ts +11 -0
  100. package/dist/agents/01-analyst.d.ts.map +1 -0
  101. package/dist/agents/01-analyst.js +75 -0
  102. package/dist/agents/01-analyst.js.map +1 -0
  103. package/dist/agents/02-seed-architect.d.ts +11 -0
  104. package/dist/agents/02-seed-architect.d.ts.map +1 -0
  105. package/dist/agents/02-seed-architect.js +51 -0
  106. package/dist/agents/02-seed-architect.js.map +1 -0
  107. package/dist/agents/03-test-generator.d.ts +9 -0
  108. package/dist/agents/03-test-generator.d.ts.map +1 -0
  109. package/dist/agents/03-test-generator.js +167 -0
  110. package/dist/agents/03-test-generator.js.map +1 -0
  111. package/dist/agents/04-unit-runner.d.ts +9 -0
  112. package/dist/agents/04-unit-runner.d.ts.map +1 -0
  113. package/dist/agents/04-unit-runner.js +113 -0
  114. package/dist/agents/04-unit-runner.js.map +1 -0
  115. package/dist/agents/05-browser-crawler.d.ts +30 -0
  116. package/dist/agents/05-browser-crawler.d.ts.map +1 -0
  117. package/dist/agents/05-browser-crawler.js +685 -0
  118. package/dist/agents/05-browser-crawler.js.map +1 -0
  119. package/dist/agents/06-api-exerciser.d.ts +23 -0
  120. package/dist/agents/06-api-exerciser.d.ts.map +1 -0
  121. package/dist/agents/06-api-exerciser.js +253 -0
  122. package/dist/agents/06-api-exerciser.js.map +1 -0
  123. package/dist/agents/07-security-scout.d.ts +11 -0
  124. package/dist/agents/07-security-scout.d.ts.map +1 -0
  125. package/dist/agents/07-security-scout.js +142 -0
  126. package/dist/agents/07-security-scout.js.map +1 -0
  127. package/dist/agents/08-a11y-guardian.d.ts +13 -0
  128. package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
  129. package/dist/agents/08-a11y-guardian.js +176 -0
  130. package/dist/agents/08-a11y-guardian.js.map +1 -0
  131. package/dist/agents/09-healer.d.ts +33 -0
  132. package/dist/agents/09-healer.d.ts.map +1 -0
  133. package/dist/agents/09-healer.js +167 -0
  134. package/dist/agents/09-healer.js.map +1 -0
  135. package/dist/agents/10-reporter.d.ts +26 -0
  136. package/dist/agents/10-reporter.d.ts.map +1 -0
  137. package/dist/agents/10-reporter.js +215 -0
  138. package/dist/agents/10-reporter.js.map +1 -0
  139. package/dist/agents/11-fixer.d.ts +26 -0
  140. package/dist/agents/11-fixer.d.ts.map +1 -0
  141. package/dist/agents/11-fixer.js +195 -0
  142. package/dist/agents/11-fixer.js.map +1 -0
  143. package/dist/agents/12-ux-inspector.d.ts +15 -0
  144. package/dist/agents/12-ux-inspector.d.ts.map +1 -0
  145. package/dist/agents/12-ux-inspector.js +364 -0
  146. package/dist/agents/12-ux-inspector.js.map +1 -0
  147. package/dist/agents/13-performance-profiler.d.ts +13 -0
  148. package/dist/agents/13-performance-profiler.d.ts.map +1 -0
  149. package/dist/agents/13-performance-profiler.js +216 -0
  150. package/dist/agents/13-performance-profiler.js.map +1 -0
  151. package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
  152. package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
  153. package/dist/agents/14-data-integrity-auditor.js +356 -0
  154. package/dist/agents/14-data-integrity-auditor.js.map +1 -0
  155. package/dist/agents/15-regression-sentinel.d.ts +25 -0
  156. package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
  157. package/dist/agents/15-regression-sentinel.js +251 -0
  158. package/dist/agents/15-regression-sentinel.js.map +1 -0
  159. package/dist/agents/16-chaos-agent.d.ts +9 -0
  160. package/dist/agents/16-chaos-agent.d.ts.map +1 -0
  161. package/dist/agents/16-chaos-agent.js +207 -0
  162. package/dist/agents/16-chaos-agent.js.map +1 -0
  163. package/dist/agents/17-documentation-validator.d.ts +31 -0
  164. package/dist/agents/17-documentation-validator.d.ts.map +1 -0
  165. package/dist/agents/17-documentation-validator.js +246 -0
  166. package/dist/agents/17-documentation-validator.js.map +1 -0
  167. package/dist/agents/18-integration-watchdog.d.ts +10 -0
  168. package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
  169. package/dist/agents/18-integration-watchdog.js +138 -0
  170. package/dist/agents/18-integration-watchdog.js.map +1 -0
  171. package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
  172. package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
  173. package/dist/agents/19-tenant-isolation-auditor.js +166 -0
  174. package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
  175. package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
  176. package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
  177. package/dist/agents/20-workflow-completion-tester.js +159 -0
  178. package/dist/agents/20-workflow-completion-tester.js.map +1 -0
  179. package/dist/agents/21-state-session-tester.d.ts +10 -0
  180. package/dist/agents/21-state-session-tester.d.ts.map +1 -0
  181. package/dist/agents/21-state-session-tester.js +233 -0
  182. package/dist/agents/21-state-session-tester.js.map +1 -0
  183. package/dist/agents/22-email-notification-verifier.d.ts +11 -0
  184. package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
  185. package/dist/agents/22-email-notification-verifier.js +199 -0
  186. package/dist/agents/22-email-notification-verifier.js.map +1 -0
  187. package/dist/agents/23-migration-tester.d.ts +10 -0
  188. package/dist/agents/23-migration-tester.d.ts.map +1 -0
  189. package/dist/agents/23-migration-tester.js +74 -0
  190. package/dist/agents/23-migration-tester.js.map +1 -0
  191. package/dist/agents/base-agent.d.ts +19 -0
  192. package/dist/agents/base-agent.d.ts.map +1 -0
  193. package/dist/agents/base-agent.js +67 -0
  194. package/dist/agents/base-agent.js.map +1 -0
  195. package/dist/agents/registry.d.ts +29 -0
  196. package/dist/agents/registry.d.ts.map +1 -0
  197. package/dist/agents/registry.js +117 -0
  198. package/dist/agents/registry.js.map +1 -0
  199. package/dist/core/ci-output.d.ts +35 -0
  200. package/dist/core/ci-output.d.ts.map +1 -0
  201. package/dist/core/ci-output.js +193 -0
  202. package/dist/core/ci-output.js.map +1 -0
  203. package/dist/core/cli.d.ts +11 -0
  204. package/dist/core/cli.d.ts.map +1 -0
  205. package/dist/core/cli.js +197 -0
  206. package/dist/core/cli.js.map +1 -0
  207. package/dist/core/config.d.ts +111 -0
  208. package/dist/core/config.d.ts.map +1 -0
  209. package/dist/core/config.js +42 -0
  210. package/dist/core/config.js.map +1 -0
  211. package/dist/core/cost-tracker.d.ts +22 -0
  212. package/dist/core/cost-tracker.d.ts.map +1 -0
  213. package/dist/core/cost-tracker.js +41 -0
  214. package/dist/core/cost-tracker.js.map +1 -0
  215. package/dist/core/evidence.d.ts +28 -0
  216. package/dist/core/evidence.d.ts.map +1 -0
  217. package/dist/core/evidence.js +95 -0
  218. package/dist/core/evidence.js.map +1 -0
  219. package/dist/core/fix-loop.d.ts +29 -0
  220. package/dist/core/fix-loop.d.ts.map +1 -0
  221. package/dist/core/fix-loop.js +70 -0
  222. package/dist/core/fix-loop.js.map +1 -0
  223. package/dist/core/health-check.d.ts +21 -0
  224. package/dist/core/health-check.d.ts.map +1 -0
  225. package/dist/core/health-check.js +26 -0
  226. package/dist/core/health-check.js.map +1 -0
  227. package/dist/core/init.d.ts +2 -0
  228. package/dist/core/init.d.ts.map +1 -0
  229. package/dist/core/init.js +435 -0
  230. package/dist/core/init.js.map +1 -0
  231. package/dist/core/license-gen.d.ts +12 -0
  232. package/dist/core/license-gen.d.ts.map +1 -0
  233. package/dist/core/license-gen.js +169 -0
  234. package/dist/core/license-gen.js.map +1 -0
  235. package/dist/core/license.d.ts +33 -0
  236. package/dist/core/license.d.ts.map +1 -0
  237. package/dist/core/license.js +170 -0
  238. package/dist/core/license.js.map +1 -0
  239. package/dist/core/messages.d.ts +10 -0
  240. package/dist/core/messages.d.ts.map +1 -0
  241. package/dist/core/messages.js +47 -0
  242. package/dist/core/messages.js.map +1 -0
  243. package/dist/core/multi-browser.d.ts +36 -0
  244. package/dist/core/multi-browser.d.ts.map +1 -0
  245. package/dist/core/multi-browser.js +88 -0
  246. package/dist/core/multi-browser.js.map +1 -0
  247. package/dist/core/orchestrator.d.ts +48 -0
  248. package/dist/core/orchestrator.d.ts.map +1 -0
  249. package/dist/core/orchestrator.js +291 -0
  250. package/dist/core/orchestrator.js.map +1 -0
  251. package/dist/core/phase-gate.d.ts +4 -0
  252. package/dist/core/phase-gate.d.ts.map +1 -0
  253. package/dist/core/phase-gate.js +39 -0
  254. package/dist/core/phase-gate.js.map +1 -0
  255. package/dist/core/report-html.d.ts +9 -0
  256. package/dist/core/report-html.d.ts.map +1 -0
  257. package/dist/core/report-html.js +617 -0
  258. package/dist/core/report-html.js.map +1 -0
  259. package/dist/core/report-upload.d.ts +16 -0
  260. package/dist/core/report-upload.d.ts.map +1 -0
  261. package/dist/core/report-upload.js +124 -0
  262. package/dist/core/report-upload.js.map +1 -0
  263. package/dist/core/run-counter.d.ts +40 -0
  264. package/dist/core/run-counter.d.ts.map +1 -0
  265. package/dist/core/run-counter.js +120 -0
  266. package/dist/core/run-counter.js.map +1 -0
  267. package/dist/core/types.d.ts +53 -0
  268. package/dist/core/types.d.ts.map +1 -0
  269. package/dist/core/types.js +2 -0
  270. package/dist/core/types.js.map +1 -0
  271. package/dist/helpers/api-client.d.ts +30 -0
  272. package/dist/helpers/api-client.d.ts.map +1 -0
  273. package/dist/helpers/api-client.js +77 -0
  274. package/dist/helpers/api-client.js.map +1 -0
  275. package/dist/helpers/element-discovery.d.ts +18 -0
  276. package/dist/helpers/element-discovery.d.ts.map +1 -0
  277. package/dist/helpers/element-discovery.js +82 -0
  278. package/dist/helpers/element-discovery.js.map +1 -0
  279. package/dist/helpers/env-resolver.d.ts +29 -0
  280. package/dist/helpers/env-resolver.d.ts.map +1 -0
  281. package/dist/helpers/env-resolver.js +51 -0
  282. package/dist/helpers/env-resolver.js.map +1 -0
  283. package/dist/helpers/form-filler.d.ts +13 -0
  284. package/dist/helpers/form-filler.d.ts.map +1 -0
  285. package/dist/helpers/form-filler.js +98 -0
  286. package/dist/helpers/form-filler.js.map +1 -0
  287. package/dist/helpers/modal-handler.d.ts +16 -0
  288. package/dist/helpers/modal-handler.d.ts.map +1 -0
  289. package/dist/helpers/modal-handler.js +95 -0
  290. package/dist/helpers/modal-handler.js.map +1 -0
  291. package/dist/helpers/navigation.d.ts +37 -0
  292. package/dist/helpers/navigation.d.ts.map +1 -0
  293. package/dist/helpers/navigation.js +83 -0
  294. package/dist/helpers/navigation.js.map +1 -0
  295. package/dist/helpers/quality-gate.d.ts +17 -0
  296. package/dist/helpers/quality-gate.d.ts.map +1 -0
  297. package/dist/helpers/quality-gate.js +144 -0
  298. package/dist/helpers/quality-gate.js.map +1 -0
  299. package/dist/helpers/screenshot.d.ts +24 -0
  300. package/dist/helpers/screenshot.d.ts.map +1 -0
  301. package/dist/helpers/screenshot.js +76 -0
  302. package/dist/helpers/screenshot.js.map +1 -0
  303. package/dist/helpers/seed-validator.d.ts +15 -0
  304. package/dist/helpers/seed-validator.d.ts.map +1 -0
  305. package/dist/helpers/seed-validator.js +53 -0
  306. package/dist/helpers/seed-validator.js.map +1 -0
  307. package/helpers/__tests__/api-client.test.ts +199 -0
  308. package/helpers/__tests__/element-discovery.test.ts +202 -0
  309. package/helpers/__tests__/form-filler-extended.test.ts +212 -0
  310. package/helpers/__tests__/form-filler.test.ts +99 -0
  311. package/helpers/__tests__/modal-handler.test.ts +152 -0
  312. package/helpers/__tests__/navigation.test.ts +214 -0
  313. package/helpers/__tests__/quality-gate.test.ts +117 -0
  314. package/helpers/__tests__/screenshot.test.ts +139 -0
  315. package/helpers/__tests__/seed-validator.test.ts +114 -0
  316. package/helpers/api-client.ts +111 -0
  317. package/helpers/element-discovery.ts +105 -0
  318. package/helpers/env-resolver.ts +69 -0
  319. package/helpers/form-filler.ts +126 -0
  320. package/helpers/modal-handler.ts +108 -0
  321. package/helpers/navigation.ts +100 -0
  322. package/helpers/quality-gate.ts +180 -0
  323. package/helpers/screenshot.ts +111 -0
  324. package/helpers/seed-validator.ts +70 -0
  325. package/package.json +88 -0
@@ -0,0 +1,76 @@
1
+ import { mkdir, copyFile, readFile, writeFile, stat } from 'node:fs/promises';
2
+ import { join, basename } from 'path';
3
+ import pixelmatch from 'pixelmatch';
4
+ import { PNG } from 'pngjs';
5
+ async function fileExists(p) {
6
+ try {
7
+ await stat(p);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ /**
15
+ * Captures a screenshot of the current page state and saves it to outputDir.
16
+ * Returns the absolute file path of the saved screenshot.
17
+ */
18
+ export async function captureScreenshot(page, name, outputDir) {
19
+ const dirExists = await fileExists(outputDir);
20
+ if (!dirExists) {
21
+ await mkdir(outputDir, { recursive: true });
22
+ }
23
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
24
+ const filePath = join(outputDir, `${safeName}.png`);
25
+ await page.screenshot({ path: filePath, fullPage: false });
26
+ return filePath;
27
+ }
28
+ /**
29
+ * Compares two PNG screenshots using pixelmatch.
30
+ * Returns match=true when the diff percentage is at or below the threshold.
31
+ * Default threshold: 0.1 (0.1% pixel difference).
32
+ * Writes a diff PNG alongside the current screenshot when images differ.
33
+ */
34
+ export async function compareScreenshots(currentPath, baselinePath, threshold = 0.1) {
35
+ const currentData = await readFile(currentPath);
36
+ const baselineData = await readFile(baselinePath);
37
+ const current = PNG.sync.read(currentData);
38
+ const baseline = PNG.sync.read(baselineData);
39
+ if (current.width !== baseline.width || current.height !== baseline.height) {
40
+ return {
41
+ match: false,
42
+ diffPercentage: 100,
43
+ };
44
+ }
45
+ const { width, height } = current;
46
+ const diff = new PNG({ width, height });
47
+ const diffPixels = pixelmatch(current.data, baseline.data, diff.data, width, height, { threshold: 0.1 });
48
+ const totalPixels = width * height;
49
+ const diffPercentage = totalPixels > 0 ? (diffPixels / totalPixels) * 100 : 0;
50
+ const match = diffPercentage <= threshold;
51
+ let diffPath;
52
+ if (!match) {
53
+ diffPath = currentPath.replace(/\.png$/, '_diff.png');
54
+ await writeFile(diffPath, PNG.sync.write(diff));
55
+ }
56
+ return {
57
+ match,
58
+ diffPercentage,
59
+ ...(diffPath ? { diffPath } : {}),
60
+ };
61
+ }
62
+ /**
63
+ * Copies the current screenshot to the baseline directory, establishing it
64
+ * as the new reference image. Returns the path of the new baseline file.
65
+ */
66
+ export async function updateBaseline(currentPath, baselineDir) {
67
+ const dirExists = await fileExists(baselineDir);
68
+ if (!dirExists) {
69
+ await mkdir(baselineDir, { recursive: true });
70
+ }
71
+ const fileName = basename(currentPath);
72
+ const baselinePath = join(baselineDir, fileName);
73
+ await copyFile(currentPath, baselinePath);
74
+ return baselinePath;
75
+ }
76
+ //# sourceMappingURL=screenshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../helpers/screenshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface SeedValidationResult {
2
+ covered: string[];
3
+ missing: string[];
4
+ }
5
+ /**
6
+ * Validates that a Prisma seed file contains data creation entries for each
7
+ * of the supplied module IDs.
8
+ *
9
+ * Detection heuristic: for each moduleId the function looks for the module
10
+ * identifier (case-insensitive) appearing in a data-creation context — i.e.
11
+ * adjacent to keywords such as create, createMany, insert, upsert, seed, or
12
+ * model within 120 characters. This covers common Prisma and raw-SQL patterns.
13
+ */
14
+ export declare function validateSeedCompleteness(seedFilePath: string, moduleIds: string[]): Promise<SeedValidationResult>;
15
+ //# sourceMappingURL=seed-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-validator.d.ts","sourceRoot":"","sources":["../../helpers/seed-validator.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAgB/B"}
@@ -0,0 +1,53 @@
1
+ import { readFileSync } from 'fs';
2
+ /**
3
+ * Validates that a Prisma seed file contains data creation entries for each
4
+ * of the supplied module IDs.
5
+ *
6
+ * Detection heuristic: for each moduleId the function looks for the module
7
+ * identifier (case-insensitive) appearing in a data-creation context — i.e.
8
+ * adjacent to keywords such as create, createMany, insert, upsert, seed, or
9
+ * model within 120 characters. This covers common Prisma and raw-SQL patterns.
10
+ */
11
+ export async function validateSeedCompleteness(seedFilePath, moduleIds) {
12
+ const content = readFileSync(seedFilePath, 'utf-8');
13
+ const contentLower = content.toLowerCase();
14
+ const covered = [];
15
+ const missing = [];
16
+ for (const moduleId of moduleIds) {
17
+ if (isCoveredByContent(contentLower, moduleId.toLowerCase())) {
18
+ covered.push(moduleId);
19
+ }
20
+ else {
21
+ missing.push(moduleId);
22
+ }
23
+ }
24
+ return { covered, missing };
25
+ }
26
+ /**
27
+ * Returns true if the seed content appears to create data for the given module.
28
+ * Checks for the module name near data-creation keywords or as a Prisma model reference.
29
+ */
30
+ function isCoveredByContent(contentLower, moduleIdLower) {
31
+ const creationKeywords = ['create', 'createmany', 'insert', 'upsert', 'seed', 'model'];
32
+ let searchStart = 0;
33
+ const moduleIndex = contentLower.indexOf(moduleIdLower, searchStart);
34
+ if (moduleIndex === -1) {
35
+ return false;
36
+ }
37
+ // Scan all occurrences of the module name for a nearby creation keyword
38
+ let idx = moduleIndex;
39
+ while (idx !== -1) {
40
+ const windowStart = Math.max(0, idx - 120);
41
+ const windowEnd = Math.min(contentLower.length, idx + moduleIdLower.length + 120);
42
+ const window = contentLower.slice(windowStart, windowEnd);
43
+ for (const keyword of creationKeywords) {
44
+ if (window.includes(keyword)) {
45
+ return true;
46
+ }
47
+ }
48
+ searchStart = idx + 1;
49
+ idx = contentLower.indexOf(moduleIdLower, searchStart);
50
+ }
51
+ return false;
52
+ }
53
+ //# sourceMappingURL=seed-validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-validator.js","sourceRoot":"","sources":["../../helpers/seed-validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAOlC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,YAAoB,EACpB,SAAmB;IAEnB,MAAM,OAAO,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAE3C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,kBAAkB,CAAC,YAAY,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YAC7D,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAS,kBAAkB,CAAC,YAAoB,EAAE,aAAqB;IACrE,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAEvF,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAErE,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,wEAAwE;IACxE,IAAI,GAAG,GAAG,WAAW,CAAC;IACtB,OAAO,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QAClB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,GAAG,GAAG,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAClF,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QAE1D,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;YACvC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC7B,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAED,WAAW,GAAG,GAAG,GAAG,CAAC,CAAC;QACtB,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IACzD,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ApiClient } from '../api-client';
3
+ import type { ApiResponse } from '../api-client';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // fetch mock helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function makeFetchResponse(
10
+ status: number,
11
+ body: unknown,
12
+ headers: Record<string, string> = {},
13
+ ): Response {
14
+ const responseHeaders = new Headers({
15
+ 'content-type': 'application/json',
16
+ ...headers,
17
+ });
18
+
19
+ return {
20
+ status,
21
+ headers: responseHeaders,
22
+ json: async () => body,
23
+ text: async () => JSON.stringify(body),
24
+ } as unknown as Response;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Tests
29
+ // ---------------------------------------------------------------------------
30
+
31
+ describe('ApiClient', () => {
32
+ beforeEach(() => {
33
+ vi.stubGlobal('fetch', vi.fn());
34
+ });
35
+
36
+ afterEach(() => {
37
+ vi.unstubAllGlobals();
38
+ });
39
+
40
+ describe('Authorization header', () => {
41
+ it('includes Bearer token in GET requests when token is provided', async () => {
42
+ const mockFetch = vi.mocked(fetch);
43
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(200, { ok: true }));
44
+
45
+ const client = new ApiClient('https://api.example.com', 'my-jwt-token');
46
+ await client.get('/api/data');
47
+
48
+ expect(mockFetch).toHaveBeenCalledOnce();
49
+ const [, init] = mockFetch.mock.calls[0];
50
+ const headers = init?.headers as Record<string, string>;
51
+ expect(headers['Authorization']).toBe('Bearer my-jwt-token');
52
+ });
53
+
54
+ it('omits Authorization header when no token is provided', async () => {
55
+ const mockFetch = vi.mocked(fetch);
56
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(200, {}));
57
+
58
+ const client = new ApiClient('https://api.example.com');
59
+ await client.get('/api/public');
60
+
61
+ const [, init] = mockFetch.mock.calls[0];
62
+ const headers = init?.headers as Record<string, string>;
63
+ expect(headers['Authorization']).toBeUndefined();
64
+ });
65
+
66
+ it('includes Bearer token in POST requests', async () => {
67
+ const mockFetch = vi.mocked(fetch);
68
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(201, { id: 1 }));
69
+
70
+ const client = new ApiClient('https://api.example.com', 'post-token');
71
+ await client.post('/api/items', { name: 'test' });
72
+
73
+ const [, init] = mockFetch.mock.calls[0];
74
+ const headers = init?.headers as Record<string, string>;
75
+ expect(headers['Authorization']).toBe('Bearer post-token');
76
+ });
77
+
78
+ it('includes Bearer token in PUT requests', async () => {
79
+ const mockFetch = vi.mocked(fetch);
80
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(200, { updated: true }));
81
+
82
+ const client = new ApiClient('https://api.example.com', 'put-token');
83
+ await client.put('/api/items/1', { name: 'updated' });
84
+
85
+ const [, init] = mockFetch.mock.calls[0];
86
+ const headers = init?.headers as Record<string, string>;
87
+ expect(headers['Authorization']).toBe('Bearer put-token');
88
+ });
89
+
90
+ it('includes Bearer token in DELETE requests', async () => {
91
+ const mockFetch = vi.mocked(fetch);
92
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(204, null));
93
+
94
+ const client = new ApiClient('https://api.example.com', 'delete-token');
95
+ await client.delete('/api/items/1');
96
+
97
+ const [, init] = mockFetch.mock.calls[0];
98
+ const headers = init?.headers as Record<string, string>;
99
+ expect(headers['Authorization']).toBe('Bearer delete-token');
100
+ });
101
+ });
102
+
103
+ describe('response timing', () => {
104
+ it('records a non-negative durationMs', async () => {
105
+ const mockFetch = vi.mocked(fetch);
106
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(200, {}));
107
+
108
+ const client = new ApiClient('https://api.example.com');
109
+ const response: ApiResponse = await client.get('/api/ping');
110
+
111
+ expect(response.durationMs).toBeGreaterThanOrEqual(0);
112
+ expect(typeof response.durationMs).toBe('number');
113
+ });
114
+ });
115
+
116
+ describe('response shape', () => {
117
+ it('returns status, body, headers, and durationMs', async () => {
118
+ const mockFetch = vi.mocked(fetch);
119
+ mockFetch.mockResolvedValueOnce(
120
+ makeFetchResponse(200, { message: 'hello' }, { 'x-request-id': 'abc123' }),
121
+ );
122
+
123
+ const client = new ApiClient('https://api.example.com');
124
+ const response = await client.get('/api/hello');
125
+
126
+ expect(response.status).toBe(200);
127
+ expect(response.body).toEqual({ message: 'hello' });
128
+ expect(response.headers).toHaveProperty('content-type');
129
+ expect(typeof response.durationMs).toBe('number');
130
+ });
131
+ });
132
+
133
+ describe('HTTP methods', () => {
134
+ it('uses GET method for get()', async () => {
135
+ vi.mocked(fetch).mockResolvedValueOnce(makeFetchResponse(200, {}));
136
+ const client = new ApiClient('https://api.example.com');
137
+ await client.get('/api/test');
138
+ const [, init] = vi.mocked(fetch).mock.calls[0];
139
+ expect(init?.method).toBe('GET');
140
+ });
141
+
142
+ it('uses POST method and sends JSON body for post()', async () => {
143
+ vi.mocked(fetch).mockResolvedValueOnce(makeFetchResponse(201, {}));
144
+ const client = new ApiClient('https://api.example.com');
145
+ await client.post('/api/test', { key: 'value' });
146
+ const [, init] = vi.mocked(fetch).mock.calls[0];
147
+ expect(init?.method).toBe('POST');
148
+ expect(init?.body).toBe(JSON.stringify({ key: 'value' }));
149
+ });
150
+
151
+ it('uses PUT method for put()', async () => {
152
+ vi.mocked(fetch).mockResolvedValueOnce(makeFetchResponse(200, {}));
153
+ const client = new ApiClient('https://api.example.com');
154
+ await client.put('/api/test/1', { updated: true });
155
+ const [, init] = vi.mocked(fetch).mock.calls[0];
156
+ expect(init?.method).toBe('PUT');
157
+ });
158
+
159
+ it('uses DELETE method for delete()', async () => {
160
+ vi.mocked(fetch).mockResolvedValueOnce(makeFetchResponse(204, null));
161
+ const client = new ApiClient('https://api.example.com');
162
+ await client.delete('/api/test/1');
163
+ const [, init] = vi.mocked(fetch).mock.calls[0];
164
+ expect(init?.method).toBe('DELETE');
165
+ });
166
+ });
167
+
168
+ describe('createAuthenticated()', () => {
169
+ it('returns an authenticated client when login succeeds with access_token', async () => {
170
+ const mockFetch = vi.mocked(fetch);
171
+ mockFetch.mockResolvedValueOnce(
172
+ makeFetchResponse(200, { access_token: 'jwt-abc' }),
173
+ );
174
+
175
+ const client = await ApiClient.createAuthenticated('https://api.example.com', {
176
+ email: 'user@example.com',
177
+ password: 'secret',
178
+ });
179
+
180
+ // Verify the token is used on the next request
181
+ mockFetch.mockResolvedValueOnce(makeFetchResponse(200, {}));
182
+ await client.get('/api/me');
183
+ const [, init] = mockFetch.mock.calls[1];
184
+ const headers = init?.headers as Record<string, string>;
185
+ expect(headers['Authorization']).toBe('Bearer jwt-abc');
186
+ });
187
+
188
+ it('throws when the login response contains no token', async () => {
189
+ vi.mocked(fetch).mockResolvedValueOnce(makeFetchResponse(401, { error: 'Unauthorized' }));
190
+
191
+ await expect(
192
+ ApiClient.createAuthenticated('https://api.example.com', {
193
+ email: 'bad@example.com',
194
+ password: 'wrong',
195
+ }),
196
+ ).rejects.toThrow(/Authentication failed/);
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,202 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ classifyAsDestructive,
4
+ discoverInteractiveElements,
5
+ } from '../element-discovery';
6
+ import type { DiscoveredElement } from '../element-discovery';
7
+ import type { Page } from 'playwright';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // classifyAsDestructive
11
+ // ---------------------------------------------------------------------------
12
+
13
+ describe('classifyAsDestructive(text)', () => {
14
+ it('returns true for "Delete client"', () => {
15
+ expect(classifyAsDestructive('Delete client')).toBe(true);
16
+ });
17
+
18
+ it('returns false for "Save changes"', () => {
19
+ expect(classifyAsDestructive('Save changes')).toBe(false);
20
+ });
21
+
22
+ it('returns true for "Remove item"', () => {
23
+ expect(classifyAsDestructive('Remove item')).toBe(true);
24
+ });
25
+
26
+ it('returns true for "Archive"', () => {
27
+ expect(classifyAsDestructive('Archive')).toBe(true);
28
+ });
29
+
30
+ it('returns true for "Clear all"', () => {
31
+ expect(classifyAsDestructive('Clear all')).toBe(true);
32
+ });
33
+
34
+ it('returns true for "destroy"', () => {
35
+ expect(classifyAsDestructive('destroy')).toBe(true);
36
+ });
37
+
38
+ it('returns true for "Reset password" (case-insensitive)', () => {
39
+ expect(classifyAsDestructive('Reset password')).toBe(true);
40
+ });
41
+
42
+ it('returns false for "Submit"', () => {
43
+ expect(classifyAsDestructive('Submit')).toBe(false);
44
+ });
45
+
46
+ it('returns false for an empty string', () => {
47
+ expect(classifyAsDestructive('')).toBe(false);
48
+ });
49
+ });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // discoverInteractiveElements — mock page
53
+ // ---------------------------------------------------------------------------
54
+
55
+ type EvaluateFn = (selector: string) => Array<{
56
+ tag: string;
57
+ role: string;
58
+ text: string;
59
+ ariaLabel: string | null;
60
+ testId: string | null;
61
+ }>;
62
+
63
+ function makeMockPage(evaluateResult: ReturnType<EvaluateFn>): Page {
64
+ return {
65
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
66
+ } as unknown as Page;
67
+ }
68
+
69
+ describe('discoverInteractiveElements(page)', () => {
70
+ it('returns a button element with correct type and selector', async () => {
71
+ const page = makeMockPage([
72
+ { tag: 'button', role: '', text: 'Save changes', ariaLabel: null, testId: null },
73
+ ]);
74
+
75
+ const results = await discoverInteractiveElements(page);
76
+
77
+ expect(results).toHaveLength(1);
78
+ expect(results[0]?.type).toBe('button');
79
+ expect(results[0]?.isDestructive).toBe(false);
80
+ expect(results[0]?.selector).toContain('button');
81
+ });
82
+
83
+ it('returns a link element with type "link"', async () => {
84
+ const page = makeMockPage([
85
+ { tag: 'a', role: '', text: 'Go to dashboard', ariaLabel: null, testId: null },
86
+ ]);
87
+
88
+ const results = await discoverInteractiveElements(page);
89
+
90
+ expect(results[0]?.type).toBe('link');
91
+ });
92
+
93
+ it('classifies a delete button as destructive', async () => {
94
+ const page = makeMockPage([
95
+ { tag: 'button', role: '', text: 'Delete client', ariaLabel: null, testId: null },
96
+ ]);
97
+
98
+ const results = await discoverInteractiveElements(page);
99
+
100
+ expect(results[0]?.isDestructive).toBe(true);
101
+ });
102
+
103
+ it('prefers data-testid selector when present', async () => {
104
+ const page = makeMockPage([
105
+ { tag: 'button', role: '', text: 'Submit', ariaLabel: null, testId: 'submit-btn' },
106
+ ]);
107
+
108
+ const results = await discoverInteractiveElements(page);
109
+
110
+ expect(results[0]?.selector).toBe('[data-testid="submit-btn"]');
111
+ });
112
+
113
+ it('falls back to aria-label selector when no data-testid', async () => {
114
+ const page = makeMockPage([
115
+ { tag: 'button', role: '', text: '', ariaLabel: 'Close dialog', testId: null },
116
+ ]);
117
+
118
+ const results = await discoverInteractiveElements(page);
119
+
120
+ expect(results[0]?.selector).toBe('[aria-label="Close dialog"]');
121
+ expect(results[0]?.ariaLabel).toBe('Close dialog');
122
+ });
123
+
124
+ it('classifies role="tab" elements with type "tab"', async () => {
125
+ const page = makeMockPage([
126
+ { tag: 'div', role: 'tab', text: 'Overview', ariaLabel: null, testId: null },
127
+ ]);
128
+
129
+ const results = await discoverInteractiveElements(page);
130
+
131
+ expect(results[0]?.type).toBe('tab');
132
+ });
133
+
134
+ it('classifies role="menuitem" elements with type "menuitem"', async () => {
135
+ const page = makeMockPage([
136
+ { tag: 'li', role: 'menuitem', text: 'Profile', ariaLabel: null, testId: null },
137
+ ]);
138
+
139
+ const results = await discoverInteractiveElements(page);
140
+
141
+ expect(results[0]?.type).toBe('menuitem');
142
+ });
143
+
144
+ it('classifies input elements with type "input"', async () => {
145
+ const page = makeMockPage([
146
+ { tag: 'input', role: '', text: '', ariaLabel: 'Email', testId: null },
147
+ ]);
148
+
149
+ const results = await discoverInteractiveElements(page);
150
+
151
+ expect(results[0]?.type).toBe('input');
152
+ });
153
+
154
+ it('deduplicates elements that resolve to the same selector', async () => {
155
+ const page = makeMockPage([
156
+ { tag: 'button', role: '', text: 'Save', ariaLabel: null, testId: 'save-btn' },
157
+ { tag: 'button', role: '', text: 'Save', ariaLabel: null, testId: 'save-btn' },
158
+ ]);
159
+
160
+ const results = await discoverInteractiveElements(page);
161
+
162
+ expect(results).toHaveLength(1);
163
+ });
164
+
165
+ it('returns multiple distinct elements', async () => {
166
+ const page = makeMockPage([
167
+ { tag: 'button', role: '', text: 'Add client', ariaLabel: null, testId: 'add-client' },
168
+ { tag: 'button', role: '', text: 'Delete client', ariaLabel: null, testId: 'delete-client' },
169
+ { tag: 'a', role: '', text: 'Home', ariaLabel: null, testId: null },
170
+ ]);
171
+
172
+ const results: DiscoveredElement[] = await discoverInteractiveElements(page);
173
+
174
+ expect(results).toHaveLength(3);
175
+
176
+ const deleteEl = results.find((r) => r.selector === '[data-testid="delete-client"]');
177
+ expect(deleteEl?.isDestructive).toBe(true);
178
+
179
+ const addEl = results.find((r) => r.selector === '[data-testid="add-client"]');
180
+ expect(addEl?.isDestructive).toBe(false);
181
+ });
182
+
183
+ it('uses tag:text() selector as last resort', async () => {
184
+ const page = makeMockPage([
185
+ { tag: 'button', role: '', text: 'Confirm', ariaLabel: null, testId: null },
186
+ ]);
187
+
188
+ const results = await discoverInteractiveElements(page);
189
+
190
+ expect(results[0]?.selector).toBe('button:text("Confirm")');
191
+ });
192
+
193
+ it('uses bare tag selector when there is no text, aria-label, or testId', async () => {
194
+ const page = makeMockPage([
195
+ { tag: 'button', role: '', text: '', ariaLabel: null, testId: null },
196
+ ]);
197
+
198
+ const results = await discoverInteractiveElements(page);
199
+
200
+ expect(results[0]?.selector).toBe('button');
201
+ });
202
+ });