@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,126 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ /**
4
+ * Generates a valid test value based on the input's name and type attributes.
5
+ * Handles common field patterns used across the target application.
6
+ */
7
+ export function generateTestValue(inputType: string, inputName: string): string {
8
+ const nameLower = inputName.toLowerCase();
9
+
10
+ if (nameLower.includes('email')) {
11
+ const random = Math.random().toString(36).slice(2, 8);
12
+ return `test-${random}@example.com`;
13
+ }
14
+
15
+ if (nameLower.includes('phone')) {
16
+ const digits = Math.floor(Math.random() * 900000 + 100000).toString();
17
+ return `+61 400 ${digits}`;
18
+ }
19
+
20
+ if (nameLower.includes('abn')) {
21
+ return generateValidAbn();
22
+ }
23
+
24
+ if (nameLower.includes('date')) {
25
+ const future = new Date();
26
+ future.setDate(future.getDate() + 30);
27
+ return future.toISOString().split('T')[0];
28
+ }
29
+
30
+ if (inputType === 'number') {
31
+ return '100';
32
+ }
33
+
34
+ const random = Math.random().toString(36).slice(2, 8);
35
+ return `Test Value ${random}`;
36
+ }
37
+
38
+ /**
39
+ * Generates a valid 11-digit Australian Business Number (ABN).
40
+ * Applies the official ABN checksum algorithm.
41
+ */
42
+ function generateValidAbn(): string {
43
+ // ABN weights for digits 1-11
44
+ const weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
45
+
46
+ // Generate random 8-digit core (positions 3-10)
47
+ // We fix the last 8 digits, then solve for digits 1-2
48
+ for (let attempt = 0; attempt < 100; attempt++) {
49
+ const digits: number[] = new Array(11).fill(0);
50
+ // Fill digits 3-10 randomly
51
+ for (let i = 2; i < 11; i++) {
52
+ digits[i] = Math.floor(Math.random() * 10);
53
+ }
54
+
55
+ // Try all combinations for digits 1-2 (first digit can't be 0 in reduced form)
56
+ for (let d1 = 1; d1 <= 9; d1++) {
57
+ for (let d2 = 0; d2 <= 9; d2++) {
58
+ digits[0] = d1;
59
+ digits[1] = d2;
60
+
61
+ // ABN validation: subtract 1 from first digit, sum weighted digits, divisible by 89
62
+ const checkDigits = [...digits];
63
+ checkDigits[0] -= 1;
64
+ const sum = checkDigits.reduce((acc, d, i) => acc + d * weights[i], 0);
65
+
66
+ if (sum % 89 === 0) {
67
+ return digits.join('');
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ // Known valid fallback ABN
74
+ return '51824753556';
75
+ }
76
+
77
+ /**
78
+ * Fills all visible, enabled form inputs within the given form selector.
79
+ * If testData is provided, uses its values for fields matching the key names;
80
+ * otherwise auto-generates values using generateTestValue.
81
+ */
82
+ export async function fillForm(
83
+ page: Page,
84
+ formSelector: string,
85
+ testData?: Record<string, string>,
86
+ ): Promise<void> {
87
+ const inputs = await page.$$(
88
+ `${formSelector} input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([disabled])`,
89
+ );
90
+
91
+ for (const input of inputs) {
92
+ const name = (await input.getAttribute('name')) ?? '';
93
+ const type = (await input.getAttribute('type')) ?? 'text';
94
+
95
+ if (testData && name && Object.prototype.hasOwnProperty.call(testData, name)) {
96
+ await input.fill(testData[name]);
97
+ continue;
98
+ }
99
+
100
+ if (type === 'checkbox' || type === 'radio') {
101
+ const checked = await input.isChecked();
102
+ if (!checked) {
103
+ await input.check();
104
+ }
105
+ continue;
106
+ }
107
+
108
+ const value = generateTestValue(type, name);
109
+ await input.fill(value);
110
+ }
111
+
112
+ const textareas = await page.$$(
113
+ `${formSelector} textarea:not([disabled])`,
114
+ );
115
+
116
+ for (const textarea of textareas) {
117
+ const name = (await textarea.getAttribute('name')) ?? '';
118
+
119
+ if (testData && name && Object.prototype.hasOwnProperty.call(testData, name)) {
120
+ await textarea.fill(testData[name]);
121
+ } else {
122
+ const random = Math.random().toString(36).slice(2, 8);
123
+ await textarea.fill(`Test textarea value ${random}`);
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,108 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ /** CSS selectors used to detect an open modal or dialog. */
4
+ const MODAL_SELECTORS = [
5
+ '[role="dialog"]',
6
+ '[role="alertdialog"]',
7
+ '.modal',
8
+ '[data-modal]',
9
+ '[data-testid="modal"]',
10
+ ];
11
+
12
+ /** CSS selectors used to locate the close (×) button inside a modal. */
13
+ const CLOSE_BUTTON_SELECTORS = [
14
+ '[aria-label="Close"]',
15
+ '[aria-label="close"]',
16
+ '[data-testid="modal-close"]',
17
+ 'button.modal-close',
18
+ 'button[class*="close"]',
19
+ '[role="dialog"] button:first-of-type',
20
+ ];
21
+
22
+ /** CSS selectors for the overlay/backdrop behind a modal. */
23
+ const OVERLAY_SELECTORS = [
24
+ '[data-testid="modal-overlay"]',
25
+ '.modal-overlay',
26
+ '.modal-backdrop',
27
+ '[class*="overlay"]',
28
+ '[class*="backdrop"]',
29
+ ];
30
+
31
+ /**
32
+ * Clicks the trigger element and waits for a modal/dialog to become visible.
33
+ * Returns true if a modal is detected within 5 seconds; false otherwise.
34
+ */
35
+ export async function openModal(page: Page, triggerSelector: string): Promise<boolean> {
36
+ await page.click(triggerSelector);
37
+
38
+ for (const selector of MODAL_SELECTORS) {
39
+ try {
40
+ await page.waitForSelector(selector, { state: 'visible', timeout: 5000 });
41
+ return true;
42
+ } catch {
43
+ // Selector not found — try the next candidate
44
+ }
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Closes the currently open modal using the specified method.
52
+ * Returns true if the modal is no longer visible after the action; false otherwise.
53
+ */
54
+ export async function closeModal(
55
+ page: Page,
56
+ method: 'x-button' | 'escape' | 'overlay',
57
+ ): Promise<boolean> {
58
+ switch (method) {
59
+ case 'x-button': {
60
+ for (const selector of CLOSE_BUTTON_SELECTORS) {
61
+ const button = page.locator(selector);
62
+ if (await button.isVisible().catch(() => false)) {
63
+ await button.click();
64
+ break;
65
+ }
66
+ }
67
+ break;
68
+ }
69
+
70
+ case 'escape': {
71
+ await page.keyboard.press('Escape');
72
+ break;
73
+ }
74
+
75
+ case 'overlay': {
76
+ for (const selector of OVERLAY_SELECTORS) {
77
+ const overlay = page.locator(selector);
78
+ if (await overlay.isVisible().catch(() => false)) {
79
+ await overlay.click({ position: { x: 1, y: 1 } });
80
+ break;
81
+ }
82
+ }
83
+ break;
84
+ }
85
+ }
86
+
87
+ // Give the DOM a moment to react then verify the modal is gone
88
+ await page.waitForTimeout(300);
89
+ return !(await isModalOpen(page));
90
+ }
91
+
92
+ /**
93
+ * Returns true if any recognised modal or dialog element is currently visible.
94
+ */
95
+ export async function isModalOpen(page: Page): Promise<boolean> {
96
+ for (const selector of MODAL_SELECTORS) {
97
+ try {
98
+ const locator = page.locator(selector);
99
+ if (await locator.isVisible()) {
100
+ return true;
101
+ }
102
+ } catch {
103
+ // Element not present — continue checking
104
+ }
105
+ }
106
+
107
+ return false;
108
+ }
@@ -0,0 +1,100 @@
1
+ import type { Page } from 'playwright';
2
+
3
+ /** Default milliseconds to wait for all loading indicators to disappear. */
4
+ const DEFAULT_WAIT_TIMEOUT_MS = 10_000;
5
+
6
+ /**
7
+ * Selectors that represent loading / skeleton states produced by React Suspense
8
+ * fallback components. All must be absent before `waitForSectionLoad` resolves.
9
+ */
10
+ const LOADING_SELECTORS = [
11
+ '[aria-label="Loading"]',
12
+ '[data-testid="loading-spinner"]',
13
+ '[data-testid="skeleton"]',
14
+ '.loading-spinner',
15
+ '.skeleton',
16
+ ] as const;
17
+
18
+ /**
19
+ * Navigate to `loginUrl`, fill the email and password inputs, submit the form,
20
+ * and wait for the browser to navigate away from the login page.
21
+ */
22
+ export async function login(
23
+ page: Page,
24
+ credentials: { email: string; password: string },
25
+ loginUrl: string,
26
+ ): Promise<void> {
27
+ await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 10000 });
28
+ await page.fill('input[type="email"], input[name="email"], #login-email', credentials.email);
29
+ await page.fill('input[type="password"], input[name="password"], #login-password', credentials.password);
30
+ // Use Promise.race — SPAs may not trigger a traditional navigation after login
31
+ await Promise.all([
32
+ page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 10000 }).catch(() => {
33
+ // SPA fallback: wait for the login form to disappear
34
+ return page.waitForSelector('input[type="email"], #login-email', { state: 'hidden', timeout: 5000 }).catch(() => {
35
+ // Graceful return — login may have failed (wrong credentials, no API)
36
+ // Caller should check page URL to determine if login succeeded
37
+ });
38
+ }),
39
+ page.click('button[type="submit"]'),
40
+ ]);
41
+ }
42
+
43
+ /**
44
+ * Resolve `moduleId` to its route via `config.modules`, navigate to that route,
45
+ * and wait for the section's lazy-loaded content to appear.
46
+ *
47
+ * @throws {Error} when `moduleId` is not found in `config.modules`.
48
+ */
49
+ export async function navigateToSection(
50
+ page: Page,
51
+ moduleId: string,
52
+ config: { modules: Array<{ id: string; route: string }> },
53
+ ): Promise<void> {
54
+ const module = config.modules.find((m) => m.id === moduleId);
55
+ if (module === undefined) {
56
+ throw new Error(
57
+ `navigateToSection: moduleId "${moduleId}" not found in config. ` +
58
+ `Available ids: [${config.modules.map((m) => m.id).join(', ')}]`,
59
+ );
60
+ }
61
+ // Resolve relative routes against the current page origin
62
+ const currentUrl = page.url();
63
+ const origin = currentUrl.startsWith('http') ? new URL(currentUrl).origin : '';
64
+ const fullUrl = module.route.startsWith('http') ? module.route : `${origin}${module.route}`;
65
+ await page.goto(fullUrl);
66
+ await waitForSectionLoad(page);
67
+ }
68
+
69
+ /**
70
+ * Wait for all React Suspense loading fallbacks (spinners, skeletons) to
71
+ * disappear, indicating that lazy-loaded section content has fully rendered.
72
+ *
73
+ * @param timeout Maximum time in milliseconds to wait (default: 10 000 ms).
74
+ */
75
+ export async function waitForSectionLoad(page: Page, timeout = DEFAULT_WAIT_TIMEOUT_MS): Promise<void> {
76
+ for (const selector of LOADING_SELECTORS) {
77
+ // waitForSelector with state "hidden" resolves immediately if the element
78
+ // is already absent, so this is safe even when the spinner never appears.
79
+ await page.waitForSelector(selector, { state: 'hidden', timeout });
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Switch between the bookkeeper portal and the client portal by manipulating
85
+ * the `portal` query-string parameter on the current page URL.
86
+ *
87
+ * @param portalQueryParam Pass `"client"` (or any portal slug) to set
88
+ * `?portal=<value>`, or `null` to remove the query parameter entirely.
89
+ */
90
+ export async function switchPortal(page: Page, portalQueryParam: string | null): Promise<void> {
91
+ const currentUrl = new URL(page.url());
92
+
93
+ if (portalQueryParam === null) {
94
+ currentUrl.searchParams.delete('portal');
95
+ } else {
96
+ currentUrl.searchParams.set('portal', portalQueryParam);
97
+ }
98
+
99
+ await page.goto(currentUrl.toString());
100
+ }
@@ -0,0 +1,180 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ export interface QualityViolation {
4
+ rule: string;
5
+ message: string;
6
+ line?: number;
7
+ severity: 'error' | 'warning';
8
+ }
9
+
10
+ // Matches `: any` type annotations
11
+ const ANY_TYPE_PATTERN = /:\s*any\b/;
12
+
13
+ // Matches function declarations and arrow functions that open a block body on the same line
14
+ const FUNCTION_START_PATTERN =
15
+ /(?:function\s+\w+\s*\([^)]*\)\s*\{|(?:\([^)]*\)|\w+)\s*=>\s*\{)/;
16
+
17
+ // Matches hardcoded secret assignments with literal string values
18
+ const HARDCODED_SECRET_PATTERN =
19
+ /(?:password|secret|apiKey|api_key|token)\s*=\s*['"][^'"]+['"]/i;
20
+
21
+ // Matches lines that safely read from process.env
22
+ const PROCESS_ENV_PATTERN = /process\.env\./;
23
+
24
+ // Maximum lines allowed inside a single function body before a violation is raised
25
+ const MAX_FUNCTION_LINES = 30;
26
+
27
+ // Maximum brace-nesting depth before a violation is raised
28
+ const MAX_NESTING_DEPTH = 3;
29
+
30
+ function isCommentLine(line: string): boolean {
31
+ const trimmed = line.trim();
32
+ return (
33
+ trimmed.startsWith('//') ||
34
+ trimmed.startsWith('*') ||
35
+ trimmed.startsWith('/*')
36
+ );
37
+ }
38
+
39
+ function checkAnyType(lines: string[]): QualityViolation[] {
40
+ const violations: QualityViolation[] = [];
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+ if (isCommentLine(line)) continue;
44
+ // Strip string literals to avoid false positives from values containing "any"
45
+ const stripped = line.replace(/(['"`])(?:(?!\1)[^\\]|\\.)*\1/g, '""');
46
+ if (ANY_TYPE_PATTERN.test(stripped)) {
47
+ violations.push({
48
+ rule: 'no-any',
49
+ message: `Avoid \`any\` type — use explicit types to preserve TypeScript safety (line ${i + 1}).`,
50
+ line: i + 1,
51
+ severity: 'error',
52
+ });
53
+ }
54
+ }
55
+ return violations;
56
+ }
57
+
58
+ function checkLongFunctions(lines: string[]): QualityViolation[] {
59
+ const violations: QualityViolation[] = [];
60
+
61
+ // Stack entries: { startLine (0-based), braceDepthAtStart }
62
+ const stack: Array<{ startLine: number; braceDepthAtStart: number }> = [];
63
+ let braceDepth = 0;
64
+
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i];
67
+ const opens = (line.match(/\{/g) ?? []).length;
68
+ const closes = (line.match(/\}/g) ?? []).length;
69
+
70
+ if (FUNCTION_START_PATTERN.test(line)) {
71
+ // Record function start; its body starts at the `{` on this line, so the
72
+ // depth before processing this line's braces is `braceDepth`.
73
+ stack.push({ startLine: i, braceDepthAtStart: braceDepth });
74
+ }
75
+
76
+ braceDepth += opens - closes;
77
+
78
+ // Pop any functions whose body just closed (depth returned to start level)
79
+ while (stack.length > 0) {
80
+ const top = stack[stack.length - 1];
81
+ if (braceDepth <= top.braceDepthAtStart) {
82
+ const functionLineCount = i - top.startLine + 1;
83
+ if (functionLineCount > MAX_FUNCTION_LINES) {
84
+ violations.push({
85
+ rule: 'max-function-lines',
86
+ message:
87
+ `Function starting at line ${top.startLine + 1} is ${functionLineCount} lines long ` +
88
+ `— keep functions under ${MAX_FUNCTION_LINES + 1} lines.`,
89
+ line: top.startLine + 1,
90
+ severity: 'warning',
91
+ });
92
+ }
93
+ stack.pop();
94
+ } else {
95
+ break;
96
+ }
97
+ }
98
+ }
99
+
100
+ return violations;
101
+ }
102
+
103
+ function checkHardcodedSecrets(lines: string[]): QualityViolation[] {
104
+ const violations: QualityViolation[] = [];
105
+ for (let i = 0; i < lines.length; i++) {
106
+ const line = lines[i];
107
+ if (isCommentLine(line)) continue;
108
+ if (PROCESS_ENV_PATTERN.test(line)) continue;
109
+ if (HARDCODED_SECRET_PATTERN.test(line)) {
110
+ violations.push({
111
+ rule: 'no-hardcoded-secrets',
112
+ message: `Hardcoded secret detected at line ${i + 1} — use environment variables instead.`,
113
+ line: i + 1,
114
+ severity: 'error',
115
+ });
116
+ }
117
+ }
118
+ return violations;
119
+ }
120
+
121
+ function checkDeepNesting(lines: string[]): QualityViolation[] {
122
+ const violations: QualityViolation[] = [];
123
+ let depth = 0;
124
+ let violationActive = false;
125
+
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const line = lines[i];
128
+ const opens = (line.match(/\{/g) ?? []).length;
129
+ const closes = (line.match(/\}/g) ?? []).length;
130
+
131
+ depth += opens - closes;
132
+
133
+ if (depth > MAX_NESTING_DEPTH && !violationActive) {
134
+ violations.push({
135
+ rule: 'max-nesting-depth',
136
+ message:
137
+ `Nesting depth exceeds ${MAX_NESTING_DEPTH} levels at line ${i + 1} ` +
138
+ `— refactor to reduce complexity.`,
139
+ line: i + 1,
140
+ severity: 'warning',
141
+ });
142
+ violationActive = true;
143
+ }
144
+
145
+ if (depth <= MAX_NESTING_DEPTH) {
146
+ violationActive = false;
147
+ }
148
+ }
149
+
150
+ return violations;
151
+ }
152
+
153
+ /**
154
+ * Validates the given source code content against AVI-OS coding quality rules.
155
+ * Returns an array of QualityViolation objects; an empty array means the code
156
+ * passes all checks.
157
+ */
158
+ export function validateCodeQuality(
159
+ fileContent: string,
160
+ filePath: string,
161
+ ): QualityViolation[] {
162
+ // filePath is available for future rule extensions (e.g. path-based exclusions)
163
+ void filePath;
164
+
165
+ const lines = fileContent.split('\n');
166
+ return [
167
+ ...checkAnyType(lines),
168
+ ...checkHardcodedSecrets(lines),
169
+ ...checkLongFunctions(lines),
170
+ ...checkDeepNesting(lines),
171
+ ];
172
+ }
173
+
174
+ /**
175
+ * Reads a file from disk and runs validateCodeQuality on its contents.
176
+ */
177
+ export function runQualityGate(filePath: string): QualityViolation[] {
178
+ const content = readFileSync(filePath, 'utf-8');
179
+ return validateCodeQuality(content, filePath);
180
+ }
@@ -0,0 +1,111 @@
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
+ import type { Page } from 'playwright';
6
+
7
+ export interface ScreenshotCompareResult {
8
+ match: boolean;
9
+ diffPercentage: number;
10
+ diffPath?: string;
11
+ }
12
+
13
+ async function fileExists(p: string): Promise<boolean> {
14
+ try { await stat(p); return true; } catch { return false; }
15
+ }
16
+
17
+ /**
18
+ * Captures a screenshot of the current page state and saves it to outputDir.
19
+ * Returns the absolute file path of the saved screenshot.
20
+ */
21
+ export async function captureScreenshot(
22
+ page: Page,
23
+ name: string,
24
+ outputDir: string,
25
+ ): Promise<string> {
26
+ const dirExists = await fileExists(outputDir);
27
+ if (!dirExists) {
28
+ await mkdir(outputDir, { recursive: true });
29
+ }
30
+
31
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
32
+ const filePath = join(outputDir, `${safeName}.png`);
33
+
34
+ await page.screenshot({ path: filePath, fullPage: false });
35
+
36
+ return filePath;
37
+ }
38
+
39
+ /**
40
+ * Compares two PNG screenshots using pixelmatch.
41
+ * Returns match=true when the diff percentage is at or below the threshold.
42
+ * Default threshold: 0.1 (0.1% pixel difference).
43
+ * Writes a diff PNG alongside the current screenshot when images differ.
44
+ */
45
+ export async function compareScreenshots(
46
+ currentPath: string,
47
+ baselinePath: string,
48
+ threshold = 0.1,
49
+ ): Promise<ScreenshotCompareResult> {
50
+ const currentData = await readFile(currentPath);
51
+ const baselineData = await readFile(baselinePath);
52
+
53
+ const current = PNG.sync.read(currentData);
54
+ const baseline = PNG.sync.read(baselineData);
55
+
56
+ if (current.width !== baseline.width || current.height !== baseline.height) {
57
+ return {
58
+ match: false,
59
+ diffPercentage: 100,
60
+ };
61
+ }
62
+
63
+ const { width, height } = current;
64
+ const diff = new PNG({ width, height });
65
+
66
+ const diffPixels = pixelmatch(
67
+ current.data,
68
+ baseline.data,
69
+ diff.data,
70
+ width,
71
+ height,
72
+ { threshold: 0.1 },
73
+ );
74
+
75
+ const totalPixels = width * height;
76
+ const diffPercentage = totalPixels > 0 ? (diffPixels / totalPixels) * 100 : 0;
77
+ const match = diffPercentage <= threshold;
78
+
79
+ let diffPath: string | undefined;
80
+ if (!match) {
81
+ diffPath = currentPath.replace(/\.png$/, '_diff.png');
82
+ await writeFile(diffPath, PNG.sync.write(diff));
83
+ }
84
+
85
+ return {
86
+ match,
87
+ diffPercentage,
88
+ ...(diffPath ? { diffPath } : {}),
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Copies the current screenshot to the baseline directory, establishing it
94
+ * as the new reference image. Returns the path of the new baseline file.
95
+ */
96
+ export async function updateBaseline(
97
+ currentPath: string,
98
+ baselineDir: string,
99
+ ): Promise<string> {
100
+ const dirExists = await fileExists(baselineDir);
101
+ if (!dirExists) {
102
+ await mkdir(baselineDir, { recursive: true });
103
+ }
104
+
105
+ const fileName = basename(currentPath);
106
+ const baselinePath = join(baselineDir, fileName);
107
+
108
+ await copyFile(currentPath, baselinePath);
109
+
110
+ return baselinePath;
111
+ }
@@ -0,0 +1,70 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ export interface SeedValidationResult {
4
+ covered: string[];
5
+ missing: string[];
6
+ }
7
+
8
+ /**
9
+ * Validates that a Prisma seed file contains data creation entries for each
10
+ * of the supplied module IDs.
11
+ *
12
+ * Detection heuristic: for each moduleId the function looks for the module
13
+ * identifier (case-insensitive) appearing in a data-creation context — i.e.
14
+ * adjacent to keywords such as create, createMany, insert, upsert, seed, or
15
+ * model within 120 characters. This covers common Prisma and raw-SQL patterns.
16
+ */
17
+ export async function validateSeedCompleteness(
18
+ seedFilePath: string,
19
+ moduleIds: string[],
20
+ ): Promise<SeedValidationResult> {
21
+ const content = readFileSync(seedFilePath, 'utf-8');
22
+ const contentLower = content.toLowerCase();
23
+
24
+ const covered: string[] = [];
25
+ const missing: string[] = [];
26
+
27
+ for (const moduleId of moduleIds) {
28
+ if (isCoveredByContent(contentLower, moduleId.toLowerCase())) {
29
+ covered.push(moduleId);
30
+ } else {
31
+ missing.push(moduleId);
32
+ }
33
+ }
34
+
35
+ return { covered, missing };
36
+ }
37
+
38
+ /**
39
+ * Returns true if the seed content appears to create data for the given module.
40
+ * Checks for the module name near data-creation keywords or as a Prisma model reference.
41
+ */
42
+ function isCoveredByContent(contentLower: string, moduleIdLower: string): boolean {
43
+ const creationKeywords = ['create', 'createmany', 'insert', 'upsert', 'seed', 'model'];
44
+
45
+ let searchStart = 0;
46
+ const moduleIndex = contentLower.indexOf(moduleIdLower, searchStart);
47
+
48
+ if (moduleIndex === -1) {
49
+ return false;
50
+ }
51
+
52
+ // Scan all occurrences of the module name for a nearby creation keyword
53
+ let idx = moduleIndex;
54
+ while (idx !== -1) {
55
+ const windowStart = Math.max(0, idx - 120);
56
+ const windowEnd = Math.min(contentLower.length, idx + moduleIdLower.length + 120);
57
+ const window = contentLower.slice(windowStart, windowEnd);
58
+
59
+ for (const keyword of creationKeywords) {
60
+ if (window.includes(keyword)) {
61
+ return true;
62
+ }
63
+ }
64
+
65
+ searchStart = idx + 1;
66
+ idx = contentLower.indexOf(moduleIdLower, searchStart);
67
+ }
68
+
69
+ return false;
70
+ }