@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,212 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { fillForm, generateTestValue } from '../form-filler';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Mock Playwright Page
6
+ // ---------------------------------------------------------------------------
7
+
8
+ interface MockElement {
9
+ getAttribute: ReturnType<typeof vi.fn>;
10
+ fill: ReturnType<typeof vi.fn>;
11
+ isChecked: ReturnType<typeof vi.fn>;
12
+ check: ReturnType<typeof vi.fn>;
13
+ }
14
+
15
+ function makeElement(overrides: Partial<Record<string, unknown>> = {}): MockElement {
16
+ return {
17
+ getAttribute: vi.fn().mockImplementation((attr: string) => {
18
+ if (attr === 'name') return overrides.name ?? '';
19
+ if (attr === 'type') return overrides.type ?? 'text';
20
+ return null;
21
+ }),
22
+ fill: vi.fn().mockResolvedValue(undefined),
23
+ isChecked: vi.fn().mockResolvedValue(overrides.checked ?? false),
24
+ check: vi.fn().mockResolvedValue(undefined),
25
+ };
26
+ }
27
+
28
+ function makeMockPage(inputs: MockElement[] = [], textareas: MockElement[] = []) {
29
+ return {
30
+ $$: vi.fn().mockImplementation((selector: string) => {
31
+ if (selector.includes('textarea')) return Promise.resolve(textareas);
32
+ return Promise.resolve(inputs);
33
+ }),
34
+ };
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // fillForm tests — uncovered branches
39
+ // ---------------------------------------------------------------------------
40
+
41
+ describe('fillForm()', () => {
42
+ it('fills nothing when no inputs or textareas exist (empty form)', async () => {
43
+ const page = makeMockPage([], []);
44
+ await fillForm(page as never, 'form');
45
+
46
+ expect(page.$$).toHaveBeenCalledTimes(2);
47
+ });
48
+
49
+ it('uses testData values when field name matches a key', async () => {
50
+ const input = makeElement({ name: 'companyName', type: 'text' });
51
+ const page = makeMockPage([input]);
52
+
53
+ await fillForm(page as never, 'form', { companyName: 'Acme Inc' });
54
+
55
+ expect(input.fill).toHaveBeenCalledWith('Acme Inc');
56
+ });
57
+
58
+ it('auto-generates value when testData is provided but field name does not match', async () => {
59
+ const input = makeElement({ name: 'otherField', type: 'text' });
60
+ const page = makeMockPage([input]);
61
+
62
+ await fillForm(page as never, 'form', { differentKey: 'value' });
63
+
64
+ // Should have called fill with an auto-generated value, not the testData value
65
+ expect(input.fill).toHaveBeenCalledTimes(1);
66
+ const filledValue = input.fill.mock.calls[0][0] as string;
67
+ expect(filledValue).toMatch(/^Test Value /);
68
+ });
69
+
70
+ it('checks unchecked checkboxes', async () => {
71
+ const checkbox = makeElement({ name: 'agree', type: 'checkbox', checked: false });
72
+ const page = makeMockPage([checkbox]);
73
+
74
+ await fillForm(page as never, 'form');
75
+
76
+ expect(checkbox.isChecked).toHaveBeenCalled();
77
+ expect(checkbox.check).toHaveBeenCalled();
78
+ });
79
+
80
+ it('does not check already-checked checkboxes', async () => {
81
+ const checkbox = makeElement({ name: 'agree', type: 'checkbox', checked: true });
82
+ const page = makeMockPage([checkbox]);
83
+
84
+ await fillForm(page as never, 'form');
85
+
86
+ expect(checkbox.isChecked).toHaveBeenCalled();
87
+ expect(checkbox.check).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('checks unchecked radio buttons', async () => {
91
+ const radio = makeElement({ name: 'size', type: 'radio', checked: false });
92
+ const page = makeMockPage([radio]);
93
+
94
+ await fillForm(page as never, 'form');
95
+
96
+ expect(radio.check).toHaveBeenCalled();
97
+ });
98
+
99
+ it('skips already-checked radio buttons', async () => {
100
+ const radio = makeElement({ name: 'size', type: 'radio', checked: true });
101
+ const page = makeMockPage([radio]);
102
+
103
+ await fillForm(page as never, 'form');
104
+
105
+ expect(radio.check).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('fills textareas with testData when name matches', async () => {
109
+ const textarea = makeElement({ name: 'notes' });
110
+ const page = makeMockPage([], [textarea]);
111
+
112
+ await fillForm(page as never, 'form', { notes: 'Some important notes' });
113
+
114
+ expect(textarea.fill).toHaveBeenCalledWith('Some important notes');
115
+ });
116
+
117
+ it('fills textareas with auto-generated value when no testData matches', async () => {
118
+ const textarea = makeElement({ name: 'description' });
119
+ const page = makeMockPage([], [textarea]);
120
+
121
+ await fillForm(page as never, 'form');
122
+
123
+ expect(textarea.fill).toHaveBeenCalledTimes(1);
124
+ const filledValue = textarea.fill.mock.calls[0][0] as string;
125
+ expect(filledValue).toMatch(/^Test textarea value /);
126
+ });
127
+
128
+ it('fills textareas with auto-generated value when testData is provided but key does not match', async () => {
129
+ const textarea = makeElement({ name: 'bio' });
130
+ const page = makeMockPage([], [textarea]);
131
+
132
+ await fillForm(page as never, 'form', { differentKey: 'x' });
133
+
134
+ const filledValue = textarea.fill.mock.calls[0][0] as string;
135
+ expect(filledValue).toMatch(/^Test textarea value /);
136
+ });
137
+
138
+ it('handles inputs with empty name attribute', async () => {
139
+ const input = makeElement({ name: '', type: 'text' });
140
+ const page = makeMockPage([input]);
141
+
142
+ // testData provided but name is empty — should not match any key, falls through to generateTestValue
143
+ await fillForm(page as never, 'form', { someKey: 'val' });
144
+
145
+ expect(input.fill).toHaveBeenCalledTimes(1);
146
+ const filledValue = input.fill.mock.calls[0][0] as string;
147
+ expect(filledValue).toMatch(/^Test Value /);
148
+ });
149
+
150
+ it('handles inputs with null name and type attributes', async () => {
151
+ const input: MockElement = {
152
+ getAttribute: vi.fn().mockReturnValue(null),
153
+ fill: vi.fn().mockResolvedValue(undefined),
154
+ isChecked: vi.fn().mockResolvedValue(false),
155
+ check: vi.fn().mockResolvedValue(undefined),
156
+ };
157
+ const page = makeMockPage([input]);
158
+
159
+ await fillForm(page as never, 'form');
160
+
161
+ // Should use default type 'text' and empty name, producing a generic value
162
+ expect(input.fill).toHaveBeenCalledTimes(1);
163
+ });
164
+
165
+ it('handles multiple inputs in a single form', async () => {
166
+ const emailInput = makeElement({ name: 'email', type: 'email' });
167
+ const phoneInput = makeElement({ name: 'phone', type: 'tel' });
168
+ const numberInput = makeElement({ name: 'quantity', type: 'number' });
169
+ const page = makeMockPage([emailInput, phoneInput, numberInput]);
170
+
171
+ await fillForm(page as never, 'form');
172
+
173
+ expect(emailInput.fill).toHaveBeenCalledTimes(1);
174
+ expect(phoneInput.fill).toHaveBeenCalledTimes(1);
175
+ expect(numberInput.fill).toHaveBeenCalledWith('100');
176
+ });
177
+
178
+ it('uses testData for textarea when provided and name matches', async () => {
179
+ const textarea = makeElement({ name: 'comment' });
180
+ const page = makeMockPage([], [textarea]);
181
+
182
+ await fillForm(page as never, '#my-form', { comment: 'Great product!' });
183
+
184
+ expect(textarea.fill).toHaveBeenCalledWith('Great product!');
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Edge cases for generateTestValue
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe('generateTestValue() edge cases', () => {
193
+ it('handles uppercase field names (EMAIL)', () => {
194
+ const value = generateTestValue('text', 'EMAIL');
195
+ expect(value).toMatch(/^test-[a-z0-9]+@example\.com$/);
196
+ });
197
+
198
+ it('handles mixed case (PhoneNumber)', () => {
199
+ const value = generateTestValue('text', 'PhoneNumber');
200
+ expect(value).toMatch(/^\+61 400 \d{6}$/);
201
+ });
202
+
203
+ it('handles ABN in camelCase (clientAbn)', () => {
204
+ const value = generateTestValue('text', 'clientAbn');
205
+ expect(value).toMatch(/^\d{11}$/);
206
+ });
207
+
208
+ it('handles date in various positions (startDate, dateOfBirth)', () => {
209
+ expect(generateTestValue('text', 'startDate')).toMatch(/^\d{4}-\d{2}-\d{2}$/);
210
+ expect(generateTestValue('text', 'dateOfBirth')).toMatch(/^\d{4}-\d{2}-\d{2}$/);
211
+ });
212
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateTestValue } from '../form-filler';
3
+
4
+ describe('generateTestValue(inputType, inputName)', () => {
5
+ describe('email pattern', () => {
6
+ it('generates a valid email when name contains "email"', () => {
7
+ const value = generateTestValue('text', 'email');
8
+ expect(value).toMatch(/^test-[a-z0-9]+@example\.com$/);
9
+ });
10
+
11
+ it('generates a valid email for "userEmail" field name', () => {
12
+ const value = generateTestValue('email', 'userEmail');
13
+ expect(value).toMatch(/^test-[a-z0-9]+@example\.com$/);
14
+ });
15
+
16
+ it('generates unique emails on successive calls', () => {
17
+ const a = generateTestValue('text', 'email');
18
+ const b = generateTestValue('text', 'email');
19
+ // Very unlikely to collide with 36^6 space
20
+ expect(a).not.toBe(b);
21
+ });
22
+ });
23
+
24
+ describe('phone pattern', () => {
25
+ it('generates an Australian mobile number when name contains "phone"', () => {
26
+ const value = generateTestValue('text', 'phone');
27
+ expect(value).toMatch(/^\+61 400 \d{6}$/);
28
+ });
29
+
30
+ it('matches "phoneNumber" field name', () => {
31
+ const value = generateTestValue('tel', 'phoneNumber');
32
+ expect(value).toMatch(/^\+61 400 \d{6}$/);
33
+ });
34
+ });
35
+
36
+ describe('ABN pattern', () => {
37
+ it('generates an 11-digit string when name contains "abn"', () => {
38
+ const value = generateTestValue('text', 'abn');
39
+ expect(value).toMatch(/^\d{11}$/);
40
+ });
41
+
42
+ it('generates a valid ABN (checksum passes)', () => {
43
+ const value = generateTestValue('text', 'abn');
44
+ const weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
45
+ const digits = value.split('').map(Number);
46
+ digits[0] -= 1;
47
+ const sum = digits.reduce((acc, d, i) => acc + d * weights[i], 0);
48
+ expect(sum % 89).toBe(0);
49
+ });
50
+
51
+ it('matches "companyAbn" field name', () => {
52
+ const value = generateTestValue('text', 'companyAbn');
53
+ expect(value).toMatch(/^\d{11}$/);
54
+ });
55
+ });
56
+
57
+ describe('date pattern', () => {
58
+ it('generates an ISO date string when name contains "date"', () => {
59
+ const value = generateTestValue('text', 'date');
60
+ expect(value).toMatch(/^\d{4}-\d{2}-\d{2}$/);
61
+ });
62
+
63
+ it('generates a date approximately 30 days in the future', () => {
64
+ const value = generateTestValue('date', 'dueDate');
65
+ const generated = new Date(value).getTime();
66
+ const now = Date.now();
67
+ const days28 = 28 * 24 * 60 * 60 * 1000;
68
+ const days32 = 32 * 24 * 60 * 60 * 1000;
69
+ expect(generated).toBeGreaterThan(now + days28);
70
+ expect(generated).toBeLessThan(now + days32);
71
+ });
72
+ });
73
+
74
+ describe('number type', () => {
75
+ it('returns "100" when input type is "number"', () => {
76
+ const value = generateTestValue('number', 'quantity');
77
+ expect(value).toBe('100');
78
+ });
79
+
80
+ it('does not use number path when name contains email despite type=number', () => {
81
+ // email name takes priority over type=number
82
+ const value = generateTestValue('number', 'email');
83
+ expect(value).toMatch(/^test-[a-z0-9]+@example\.com$/);
84
+ });
85
+ });
86
+
87
+ describe('default fallback', () => {
88
+ it('returns "Test Value {random}" for an unrecognised field', () => {
89
+ const value = generateTestValue('text', 'description');
90
+ expect(value).toMatch(/^Test Value [a-z0-9]+$/);
91
+ });
92
+
93
+ it('generates unique values on successive calls', () => {
94
+ const a = generateTestValue('text', 'notes');
95
+ const b = generateTestValue('text', 'notes');
96
+ expect(a).not.toBe(b);
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { openModal, closeModal, isModalOpen } from '../modal-handler';
3
+ import type { Page, Locator } from 'playwright';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock builder helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /** Creates a mock Locator that reports visible or not. */
10
+ function makeLocator(visible: boolean): Locator {
11
+ return {
12
+ isVisible: vi.fn().mockResolvedValue(visible),
13
+ click: vi.fn().mockResolvedValue(undefined),
14
+ } as unknown as Locator;
15
+ }
16
+
17
+ /** Creates a minimal mock Page. */
18
+ function makePage(options: {
19
+ selectorVisible?: boolean;
20
+ clickError?: boolean;
21
+ waitForSelectorError?: boolean;
22
+ }): Page {
23
+ const { selectorVisible = true, clickError = false, waitForSelectorError = false } = options;
24
+
25
+ const locator = makeLocator(selectorVisible);
26
+
27
+ return {
28
+ click: clickError
29
+ ? vi.fn().mockRejectedValue(new Error('Element not found'))
30
+ : vi.fn().mockResolvedValue(undefined),
31
+ waitForSelector: waitForSelectorError
32
+ ? vi.fn().mockRejectedValue(new Error('Timeout'))
33
+ : vi.fn().mockResolvedValue(undefined),
34
+ locator: vi.fn().mockReturnValue(locator),
35
+ keyboard: {
36
+ press: vi.fn().mockResolvedValue(undefined),
37
+ },
38
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
39
+ } as unknown as Page;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // isModalOpen()
44
+ // ---------------------------------------------------------------------------
45
+
46
+ describe('isModalOpen(page)', () => {
47
+ it('returns true when a modal selector is visible', async () => {
48
+ const page = makePage({ selectorVisible: true });
49
+ const result = await isModalOpen(page);
50
+ expect(result).toBe(true);
51
+ });
52
+
53
+ it('returns false when no modal selector is visible', async () => {
54
+ const invisibleLocator = makeLocator(false);
55
+ const page = {
56
+ locator: vi.fn().mockReturnValue(invisibleLocator),
57
+ } as unknown as Page;
58
+
59
+ const result = await isModalOpen(page);
60
+ expect(result).toBe(false);
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // openModal()
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe('openModal(page, triggerSelector)', () => {
69
+ it('returns true when modal becomes visible after click', async () => {
70
+ const page = makePage({ selectorVisible: true, waitForSelectorError: false });
71
+ const result = await openModal(page, '#open-modal-btn');
72
+
73
+ expect(result).toBe(true);
74
+ expect(page.click).toHaveBeenCalledWith('#open-modal-btn');
75
+ });
76
+
77
+ it('returns false when no modal selector becomes visible', async () => {
78
+ const page = makePage({ selectorVisible: false, waitForSelectorError: true });
79
+ const result = await openModal(page, '#open-modal-btn');
80
+
81
+ expect(result).toBe(false);
82
+ });
83
+ });
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // closeModal()
87
+ // ---------------------------------------------------------------------------
88
+
89
+ describe('closeModal(page, method)', () => {
90
+ describe('x-button method', () => {
91
+ it('clicks the close button and returns true when modal is gone', async () => {
92
+ // After close, modal is no longer visible
93
+ const closedLocator = makeLocator(false);
94
+ const visibleLocator = makeLocator(true);
95
+
96
+ let callCount = 0;
97
+ const page = {
98
+ locator: vi.fn().mockImplementation(() => {
99
+ // First call returns visible close button, subsequent calls return invisible modal
100
+ callCount++;
101
+ return callCount === 1 ? visibleLocator : closedLocator;
102
+ }),
103
+ keyboard: { press: vi.fn() },
104
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
105
+ } as unknown as Page;
106
+
107
+ const result = await closeModal(page, 'x-button');
108
+ expect(visibleLocator.click).toHaveBeenCalled();
109
+ expect(result).toBe(true);
110
+ });
111
+ });
112
+
113
+ describe('escape method', () => {
114
+ it('presses Escape and returns true when modal closes', async () => {
115
+ // Modal not visible after Escape
116
+ const closedLocator = makeLocator(false);
117
+ const page = {
118
+ locator: vi.fn().mockReturnValue(closedLocator),
119
+ keyboard: { press: vi.fn().mockResolvedValue(undefined) },
120
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
121
+ } as unknown as Page;
122
+
123
+ const result = await closeModal(page, 'escape');
124
+
125
+ expect(page.keyboard.press).toHaveBeenCalledWith('Escape');
126
+ expect(result).toBe(true);
127
+ });
128
+ });
129
+
130
+ describe('overlay method', () => {
131
+ it('clicks the overlay and returns true when modal closes', async () => {
132
+ // Visible on first check (overlay present), invisible afterwards (modal closed)
133
+ let locatorCallCount = 0;
134
+ const visibleLocator = makeLocator(true);
135
+ const closedLocator = makeLocator(false);
136
+
137
+ const page = {
138
+ locator: vi.fn().mockImplementation(() => {
139
+ locatorCallCount++;
140
+ return locatorCallCount === 1 ? visibleLocator : closedLocator;
141
+ }),
142
+ keyboard: { press: vi.fn() },
143
+ waitForTimeout: vi.fn().mockResolvedValue(undefined),
144
+ } as unknown as Page;
145
+
146
+ const result = await closeModal(page, 'overlay');
147
+
148
+ expect(visibleLocator.click).toHaveBeenCalledWith({ position: { x: 1, y: 1 } });
149
+ expect(result).toBe(true);
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { Page } from 'playwright';
3
+ import { login, navigateToSection, waitForSectionLoad, switchPortal } from '../navigation';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock Page factory
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Build a minimal Playwright Page mock that satisfies the calls made by the
11
+ * navigation helpers. All methods are jest/vitest spies so callers can assert
12
+ * on them.
13
+ */
14
+ function makeMockPage(overrides: Partial<Record<string, ReturnType<typeof vi.fn>>> = {}): Page {
15
+ return {
16
+ goto: vi.fn().mockResolvedValue(null),
17
+ fill: vi.fn().mockResolvedValue(null),
18
+ click: vi.fn().mockResolvedValue(null),
19
+ waitForNavigation: vi.fn().mockResolvedValue(null),
20
+ waitForURL: vi.fn().mockResolvedValue(null),
21
+ waitForSelector: vi.fn().mockResolvedValue(null),
22
+ url: vi.fn().mockReturnValue('https://app.example.com/dashboard'),
23
+ ...overrides,
24
+ } as unknown as Page;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Test config fixture
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const TEST_CONFIG = {
32
+ modules: [
33
+ { id: 'dashboard', route: '/dashboard' },
34
+ { id: 'invoices', route: '/invoices' },
35
+ { id: 'reports', route: '/reports' },
36
+ ],
37
+ };
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // login()
41
+ // ---------------------------------------------------------------------------
42
+
43
+ describe('login(page, credentials, loginUrl)', () => {
44
+ let page: Page;
45
+
46
+ beforeEach(() => {
47
+ page = makeMockPage();
48
+ });
49
+
50
+ it('navigates to the provided loginUrl', async () => {
51
+ await login(page, { email: 'user@example.com', password: 'secret' }, 'https://app.example.com/login');
52
+ expect(page.goto).toHaveBeenCalledWith('https://app.example.com/login', { waitUntil: 'domcontentloaded', timeout: 10000 });
53
+ });
54
+
55
+ it('fills the email input', async () => {
56
+ await login(page, { email: 'admin@example.com', password: 'pw' }, 'https://app.example.com/login');
57
+ expect(page.fill).toHaveBeenCalledWith(
58
+ expect.stringContaining('email'),
59
+ 'admin@example.com',
60
+ );
61
+ });
62
+
63
+ it('fills the password input', async () => {
64
+ await login(page, { email: 'u@e.com', password: 'hunter2' }, 'https://app.example.com/login');
65
+ expect(page.fill).toHaveBeenCalledWith(
66
+ expect.stringContaining('password'),
67
+ 'hunter2',
68
+ );
69
+ });
70
+
71
+ it('clicks the submit button', async () => {
72
+ await login(page, { email: 'u@e.com', password: 'pw' }, 'https://app.example.com/login');
73
+ expect(page.click).toHaveBeenCalledWith(expect.stringContaining('submit'));
74
+ });
75
+
76
+ it('waits for URL change after clicking submit', async () => {
77
+ await login(page, { email: 'u@e.com', password: 'pw' }, 'https://app.example.com/login');
78
+ expect(page.waitForURL).toHaveBeenCalledTimes(1);
79
+ });
80
+ });
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // navigateToSection()
84
+ // ---------------------------------------------------------------------------
85
+
86
+ describe('navigateToSection(page, moduleId, config)', () => {
87
+ let page: Page;
88
+
89
+ beforeEach(() => {
90
+ page = makeMockPage();
91
+ });
92
+
93
+ it('navigates to the correct route for a known moduleId', async () => {
94
+ await navigateToSection(page, 'invoices', TEST_CONFIG);
95
+ expect(page.goto).toHaveBeenCalledWith('https://app.example.com/invoices');
96
+ });
97
+
98
+ it('navigates to the correct route for a different known moduleId', async () => {
99
+ await navigateToSection(page, 'reports', TEST_CONFIG);
100
+ expect(page.goto).toHaveBeenCalledWith('https://app.example.com/reports');
101
+ });
102
+
103
+ it('throws when moduleId is not found in config', async () => {
104
+ await expect(
105
+ navigateToSection(page, 'nonexistent-module', TEST_CONFIG),
106
+ ).rejects.toThrow('nonexistent-module');
107
+ });
108
+
109
+ it('error message lists available module ids when moduleId is not found', async () => {
110
+ await expect(
111
+ navigateToSection(page, 'missing', TEST_CONFIG),
112
+ ).rejects.toThrow(/dashboard.*invoices.*reports/);
113
+ });
114
+
115
+ it('calls waitForSectionLoad after navigating (waitForSelector is invoked)', async () => {
116
+ await navigateToSection(page, 'dashboard', TEST_CONFIG);
117
+ // waitForSectionLoad iterates over loading selectors — at least one call expected
118
+ expect(page.waitForSelector).toHaveBeenCalled();
119
+ });
120
+
121
+ it('does not call goto when moduleId is missing', async () => {
122
+ await expect(
123
+ navigateToSection(page, 'bad-id', TEST_CONFIG),
124
+ ).rejects.toThrow();
125
+ expect(page.goto).not.toHaveBeenCalled();
126
+ });
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // waitForSectionLoad()
131
+ // ---------------------------------------------------------------------------
132
+
133
+ describe('waitForSectionLoad(page, timeout?)', () => {
134
+ let page: Page;
135
+
136
+ beforeEach(() => {
137
+ page = makeMockPage();
138
+ });
139
+
140
+ it('calls waitForSelector for each loading indicator selector', async () => {
141
+ await waitForSectionLoad(page);
142
+ // Must poll for spinners and skeletons — expect multiple selector checks
143
+ expect(page.waitForSelector).toHaveBeenCalledTimes(5);
144
+ });
145
+
146
+ it('passes state "hidden" so the wait resolves when the spinner disappears', async () => {
147
+ await waitForSectionLoad(page);
148
+ const calls = (page.waitForSelector as ReturnType<typeof vi.fn>).mock.calls;
149
+ for (const [, options] of calls) {
150
+ expect(options).toMatchObject({ state: 'hidden' });
151
+ }
152
+ });
153
+
154
+ it('uses the default timeout of 10000 ms when no timeout is provided', async () => {
155
+ await waitForSectionLoad(page);
156
+ const calls = (page.waitForSelector as ReturnType<typeof vi.fn>).mock.calls;
157
+ for (const [, options] of calls) {
158
+ expect(options).toMatchObject({ timeout: 10_000 });
159
+ }
160
+ });
161
+
162
+ it('uses the provided custom timeout', async () => {
163
+ const customTimeout = 5_000;
164
+ await waitForSectionLoad(page, customTimeout);
165
+ const calls = (page.waitForSelector as ReturnType<typeof vi.fn>).mock.calls;
166
+ for (const [, options] of calls) {
167
+ expect(options).toMatchObject({ timeout: customTimeout });
168
+ }
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // switchPortal()
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe('switchPortal(page, portalQueryParam)', () => {
177
+ it('navigates with ?portal=client when portalQueryParam is "client"', async () => {
178
+ const page = makeMockPage({
179
+ url: vi.fn().mockReturnValue('https://app.example.com/dashboard'),
180
+ });
181
+ await switchPortal(page, 'client');
182
+ expect(page.goto).toHaveBeenCalledWith(
183
+ expect.stringContaining('portal=client'),
184
+ );
185
+ });
186
+
187
+ it('preserves existing query params while adding portal param', async () => {
188
+ const page = makeMockPage({
189
+ url: vi.fn().mockReturnValue('https://app.example.com/invoices?tab=unpaid'),
190
+ });
191
+ await switchPortal(page, 'bookkeeper');
192
+ const url: string = (page.goto as ReturnType<typeof vi.fn>).mock.calls[0][0];
193
+ expect(url).toContain('tab=unpaid');
194
+ expect(url).toContain('portal=bookkeeper');
195
+ });
196
+
197
+ it('removes the portal query param when portalQueryParam is null', async () => {
198
+ const page = makeMockPage({
199
+ url: vi.fn().mockReturnValue('https://app.example.com/dashboard?portal=client&tab=all'),
200
+ });
201
+ await switchPortal(page, null);
202
+ const url: string = (page.goto as ReturnType<typeof vi.fn>).mock.calls[0][0];
203
+ expect(url).not.toContain('portal=');
204
+ expect(url).toContain('tab=all');
205
+ });
206
+
207
+ it('still calls goto when no portal param was set and null is passed', async () => {
208
+ const page = makeMockPage({
209
+ url: vi.fn().mockReturnValue('https://app.example.com/dashboard'),
210
+ });
211
+ await switchPortal(page, null);
212
+ expect(page.goto).toHaveBeenCalledTimes(1);
213
+ });
214
+ });