@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.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/agents/01-analyst.ts +100 -0
- package/agents/02-seed-architect.ts +59 -0
- package/agents/03-test-generator.ts +191 -0
- package/agents/04-unit-runner.ts +160 -0
- package/agents/05-browser-crawler.ts +790 -0
- package/agents/06-api-exerciser.ts +311 -0
- package/agents/07-security-scout.ts +188 -0
- package/agents/08-a11y-guardian.ts +212 -0
- package/agents/09-healer.ts +228 -0
- package/agents/10-reporter.ts +266 -0
- package/agents/11-fixer.ts +253 -0
- package/agents/12-ux-inspector.ts +444 -0
- package/agents/13-performance-profiler.ts +271 -0
- package/agents/14-data-integrity-auditor.ts +417 -0
- package/agents/15-regression-sentinel.ts +307 -0
- package/agents/16-chaos-agent.ts +228 -0
- package/agents/17-documentation-validator.ts +266 -0
- package/agents/18-integration-watchdog.ts +178 -0
- package/agents/19-tenant-isolation-auditor.ts +199 -0
- package/agents/20-workflow-completion-tester.ts +203 -0
- package/agents/21-state-session-tester.ts +262 -0
- package/agents/22-email-notification-verifier.ts +244 -0
- package/agents/23-migration-tester.ts +80 -0
- package/agents/__tests__/01-analyst.test.ts +188 -0
- package/agents/__tests__/02-seed-architect.test.ts +152 -0
- package/agents/__tests__/03-test-generator-full.test.ts +321 -0
- package/agents/__tests__/03-test-generator.test.ts +318 -0
- package/agents/__tests__/04-unit-runner.test.ts +320 -0
- package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
- package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
- package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
- package/agents/__tests__/05-browser-crawler.test.ts +518 -0
- package/agents/__tests__/06-api-exerciser.test.ts +619 -0
- package/agents/__tests__/07-security-scout.test.ts +382 -0
- package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
- package/agents/__tests__/09-healer.test.ts +384 -0
- package/agents/__tests__/10-reporter.test.ts +366 -0
- package/agents/__tests__/11-fixer.test.ts +406 -0
- package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
- package/agents/__tests__/12-ux-inspector.test.ts +443 -0
- package/agents/__tests__/13-performance-profiler.test.ts +411 -0
- package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
- package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
- package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
- package/agents/__tests__/16-chaos-agent.test.ts +427 -0
- package/agents/__tests__/17-documentation-validator.test.ts +402 -0
- package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
- package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
- package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
- package/agents/__tests__/21-state-session-tester.test.ts +374 -0
- package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
- package/agents/__tests__/23-migration-tester.test.ts +145 -0
- package/agents/__tests__/base-agent.test.ts +188 -0
- package/agents/__tests__/registry.test.ts +218 -0
- package/agents/base-agent.ts +77 -0
- package/agents/registry.ts +136 -0
- package/baselines/api-schemas/.gitkeep +0 -0
- package/baselines/performance/.gitkeep +0 -0
- package/baselines/screenshots/.gitkeep +0 -0
- package/bin/testteam.js +10 -0
- package/core/__tests__/ci-output.test.ts +430 -0
- package/core/__tests__/cli.test.ts +387 -0
- package/core/__tests__/config.test.ts +78 -0
- package/core/__tests__/cost-tracker.test.ts +158 -0
- package/core/__tests__/evidence.test.ts +265 -0
- package/core/__tests__/fix-loop.test.ts +210 -0
- package/core/__tests__/health-check.test.ts +44 -0
- package/core/__tests__/init.test.ts +609 -0
- package/core/__tests__/integration.test.ts +204 -0
- package/core/__tests__/license-gen.test.ts +227 -0
- package/core/__tests__/license.test.ts +326 -0
- package/core/__tests__/multi-browser.test.ts +278 -0
- package/core/__tests__/orchestrator.test.ts +519 -0
- package/core/__tests__/phase-gate.test.ts +43 -0
- package/core/__tests__/report-html.test.ts +398 -0
- package/core/__tests__/report-upload.test.ts +325 -0
- package/core/__tests__/run-counter.test.ts +234 -0
- package/core/ci-output.ts +240 -0
- package/core/cli.ts +232 -0
- package/core/config.ts +178 -0
- package/core/cost-tracker.ts +59 -0
- package/core/evidence.ts +132 -0
- package/core/fix-loop.ts +85 -0
- package/core/health-check.ts +54 -0
- package/core/init.ts +546 -0
- package/core/license-gen.ts +212 -0
- package/core/license.ts +211 -0
- package/core/messages.ts +67 -0
- package/core/multi-browser.ts +136 -0
- package/core/orchestrator.ts +354 -0
- package/core/phase-gate.ts +55 -0
- package/core/report-html.ts +657 -0
- package/core/report-upload.ts +188 -0
- package/core/run-counter.ts +175 -0
- package/core/types.ts +57 -0
- package/dist/agents/01-analyst.d.ts +11 -0
- package/dist/agents/01-analyst.d.ts.map +1 -0
- package/dist/agents/01-analyst.js +75 -0
- package/dist/agents/01-analyst.js.map +1 -0
- package/dist/agents/02-seed-architect.d.ts +11 -0
- package/dist/agents/02-seed-architect.d.ts.map +1 -0
- package/dist/agents/02-seed-architect.js +51 -0
- package/dist/agents/02-seed-architect.js.map +1 -0
- package/dist/agents/03-test-generator.d.ts +9 -0
- package/dist/agents/03-test-generator.d.ts.map +1 -0
- package/dist/agents/03-test-generator.js +167 -0
- package/dist/agents/03-test-generator.js.map +1 -0
- package/dist/agents/04-unit-runner.d.ts +9 -0
- package/dist/agents/04-unit-runner.d.ts.map +1 -0
- package/dist/agents/04-unit-runner.js +113 -0
- package/dist/agents/04-unit-runner.js.map +1 -0
- package/dist/agents/05-browser-crawler.d.ts +30 -0
- package/dist/agents/05-browser-crawler.d.ts.map +1 -0
- package/dist/agents/05-browser-crawler.js +685 -0
- package/dist/agents/05-browser-crawler.js.map +1 -0
- package/dist/agents/06-api-exerciser.d.ts +23 -0
- package/dist/agents/06-api-exerciser.d.ts.map +1 -0
- package/dist/agents/06-api-exerciser.js +253 -0
- package/dist/agents/06-api-exerciser.js.map +1 -0
- package/dist/agents/07-security-scout.d.ts +11 -0
- package/dist/agents/07-security-scout.d.ts.map +1 -0
- package/dist/agents/07-security-scout.js +142 -0
- package/dist/agents/07-security-scout.js.map +1 -0
- package/dist/agents/08-a11y-guardian.d.ts +13 -0
- package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
- package/dist/agents/08-a11y-guardian.js +176 -0
- package/dist/agents/08-a11y-guardian.js.map +1 -0
- package/dist/agents/09-healer.d.ts +33 -0
- package/dist/agents/09-healer.d.ts.map +1 -0
- package/dist/agents/09-healer.js +167 -0
- package/dist/agents/09-healer.js.map +1 -0
- package/dist/agents/10-reporter.d.ts +26 -0
- package/dist/agents/10-reporter.d.ts.map +1 -0
- package/dist/agents/10-reporter.js +215 -0
- package/dist/agents/10-reporter.js.map +1 -0
- package/dist/agents/11-fixer.d.ts +26 -0
- package/dist/agents/11-fixer.d.ts.map +1 -0
- package/dist/agents/11-fixer.js +195 -0
- package/dist/agents/11-fixer.js.map +1 -0
- package/dist/agents/12-ux-inspector.d.ts +15 -0
- package/dist/agents/12-ux-inspector.d.ts.map +1 -0
- package/dist/agents/12-ux-inspector.js +364 -0
- package/dist/agents/12-ux-inspector.js.map +1 -0
- package/dist/agents/13-performance-profiler.d.ts +13 -0
- package/dist/agents/13-performance-profiler.d.ts.map +1 -0
- package/dist/agents/13-performance-profiler.js +216 -0
- package/dist/agents/13-performance-profiler.js.map +1 -0
- package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
- package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
- package/dist/agents/14-data-integrity-auditor.js +356 -0
- package/dist/agents/14-data-integrity-auditor.js.map +1 -0
- package/dist/agents/15-regression-sentinel.d.ts +25 -0
- package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
- package/dist/agents/15-regression-sentinel.js +251 -0
- package/dist/agents/15-regression-sentinel.js.map +1 -0
- package/dist/agents/16-chaos-agent.d.ts +9 -0
- package/dist/agents/16-chaos-agent.d.ts.map +1 -0
- package/dist/agents/16-chaos-agent.js +207 -0
- package/dist/agents/16-chaos-agent.js.map +1 -0
- package/dist/agents/17-documentation-validator.d.ts +31 -0
- package/dist/agents/17-documentation-validator.d.ts.map +1 -0
- package/dist/agents/17-documentation-validator.js +246 -0
- package/dist/agents/17-documentation-validator.js.map +1 -0
- package/dist/agents/18-integration-watchdog.d.ts +10 -0
- package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
- package/dist/agents/18-integration-watchdog.js +138 -0
- package/dist/agents/18-integration-watchdog.js.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.js +166 -0
- package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
- package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
- package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
- package/dist/agents/20-workflow-completion-tester.js +159 -0
- package/dist/agents/20-workflow-completion-tester.js.map +1 -0
- package/dist/agents/21-state-session-tester.d.ts +10 -0
- package/dist/agents/21-state-session-tester.d.ts.map +1 -0
- package/dist/agents/21-state-session-tester.js +233 -0
- package/dist/agents/21-state-session-tester.js.map +1 -0
- package/dist/agents/22-email-notification-verifier.d.ts +11 -0
- package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
- package/dist/agents/22-email-notification-verifier.js +199 -0
- package/dist/agents/22-email-notification-verifier.js.map +1 -0
- package/dist/agents/23-migration-tester.d.ts +10 -0
- package/dist/agents/23-migration-tester.d.ts.map +1 -0
- package/dist/agents/23-migration-tester.js +74 -0
- package/dist/agents/23-migration-tester.js.map +1 -0
- package/dist/agents/base-agent.d.ts +19 -0
- package/dist/agents/base-agent.d.ts.map +1 -0
- package/dist/agents/base-agent.js +67 -0
- package/dist/agents/base-agent.js.map +1 -0
- package/dist/agents/registry.d.ts +29 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +117 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/core/ci-output.d.ts +35 -0
- package/dist/core/ci-output.d.ts.map +1 -0
- package/dist/core/ci-output.js +193 -0
- package/dist/core/ci-output.js.map +1 -0
- package/dist/core/cli.d.ts +11 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +197 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +111 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cost-tracker.d.ts +22 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +41 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/evidence.d.ts +28 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +95 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/fix-loop.d.ts +29 -0
- package/dist/core/fix-loop.d.ts.map +1 -0
- package/dist/core/fix-loop.js +70 -0
- package/dist/core/fix-loop.js.map +1 -0
- package/dist/core/health-check.d.ts +21 -0
- package/dist/core/health-check.d.ts.map +1 -0
- package/dist/core/health-check.js +26 -0
- package/dist/core/health-check.js.map +1 -0
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +435 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/license-gen.d.ts +12 -0
- package/dist/core/license-gen.d.ts.map +1 -0
- package/dist/core/license-gen.js +169 -0
- package/dist/core/license-gen.js.map +1 -0
- package/dist/core/license.d.ts +33 -0
- package/dist/core/license.d.ts.map +1 -0
- package/dist/core/license.js +170 -0
- package/dist/core/license.js.map +1 -0
- package/dist/core/messages.d.ts +10 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js +47 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/multi-browser.d.ts +36 -0
- package/dist/core/multi-browser.d.ts.map +1 -0
- package/dist/core/multi-browser.js +88 -0
- package/dist/core/multi-browser.js.map +1 -0
- package/dist/core/orchestrator.d.ts +48 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +291 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/phase-gate.d.ts +4 -0
- package/dist/core/phase-gate.d.ts.map +1 -0
- package/dist/core/phase-gate.js +39 -0
- package/dist/core/phase-gate.js.map +1 -0
- package/dist/core/report-html.d.ts +9 -0
- package/dist/core/report-html.d.ts.map +1 -0
- package/dist/core/report-html.js +617 -0
- package/dist/core/report-html.js.map +1 -0
- package/dist/core/report-upload.d.ts +16 -0
- package/dist/core/report-upload.d.ts.map +1 -0
- package/dist/core/report-upload.js +124 -0
- package/dist/core/report-upload.js.map +1 -0
- package/dist/core/run-counter.d.ts +40 -0
- package/dist/core/run-counter.d.ts.map +1 -0
- package/dist/core/run-counter.js +120 -0
- package/dist/core/run-counter.js.map +1 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/helpers/api-client.d.ts +30 -0
- package/dist/helpers/api-client.d.ts.map +1 -0
- package/dist/helpers/api-client.js +77 -0
- package/dist/helpers/api-client.js.map +1 -0
- package/dist/helpers/element-discovery.d.ts +18 -0
- package/dist/helpers/element-discovery.d.ts.map +1 -0
- package/dist/helpers/element-discovery.js +82 -0
- package/dist/helpers/element-discovery.js.map +1 -0
- package/dist/helpers/env-resolver.d.ts +29 -0
- package/dist/helpers/env-resolver.d.ts.map +1 -0
- package/dist/helpers/env-resolver.js +51 -0
- package/dist/helpers/env-resolver.js.map +1 -0
- package/dist/helpers/form-filler.d.ts +13 -0
- package/dist/helpers/form-filler.d.ts.map +1 -0
- package/dist/helpers/form-filler.js +98 -0
- package/dist/helpers/form-filler.js.map +1 -0
- package/dist/helpers/modal-handler.d.ts +16 -0
- package/dist/helpers/modal-handler.d.ts.map +1 -0
- package/dist/helpers/modal-handler.js +95 -0
- package/dist/helpers/modal-handler.js.map +1 -0
- package/dist/helpers/navigation.d.ts +37 -0
- package/dist/helpers/navigation.d.ts.map +1 -0
- package/dist/helpers/navigation.js +83 -0
- package/dist/helpers/navigation.js.map +1 -0
- package/dist/helpers/quality-gate.d.ts +17 -0
- package/dist/helpers/quality-gate.d.ts.map +1 -0
- package/dist/helpers/quality-gate.js +144 -0
- package/dist/helpers/quality-gate.js.map +1 -0
- package/dist/helpers/screenshot.d.ts +24 -0
- package/dist/helpers/screenshot.d.ts.map +1 -0
- package/dist/helpers/screenshot.js +76 -0
- package/dist/helpers/screenshot.js.map +1 -0
- package/dist/helpers/seed-validator.d.ts +15 -0
- package/dist/helpers/seed-validator.d.ts.map +1 -0
- package/dist/helpers/seed-validator.js +53 -0
- package/dist/helpers/seed-validator.js.map +1 -0
- package/helpers/__tests__/api-client.test.ts +199 -0
- package/helpers/__tests__/element-discovery.test.ts +202 -0
- package/helpers/__tests__/form-filler-extended.test.ts +212 -0
- package/helpers/__tests__/form-filler.test.ts +99 -0
- package/helpers/__tests__/modal-handler.test.ts +152 -0
- package/helpers/__tests__/navigation.test.ts +214 -0
- package/helpers/__tests__/quality-gate.test.ts +117 -0
- package/helpers/__tests__/screenshot.test.ts +139 -0
- package/helpers/__tests__/seed-validator.test.ts +114 -0
- package/helpers/api-client.ts +111 -0
- package/helpers/element-discovery.ts +105 -0
- package/helpers/env-resolver.ts +69 -0
- package/helpers/form-filler.ts +126 -0
- package/helpers/modal-handler.ts +108 -0
- package/helpers/navigation.ts +100 -0
- package/helpers/quality-gate.ts +180 -0
- package/helpers/screenshot.ts +111 -0
- package/helpers/seed-validator.ts +70 -0
- 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
|
+
});
|