@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,578 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Stub config with UAT credentials
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
const stubConfig: ValidatedConfig = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
name: 'test',
|
|
11
|
+
auth: {
|
|
12
|
+
loginUrl: 'http://localhost/login',
|
|
13
|
+
credentials: {
|
|
14
|
+
admin: { email: 'admin@test.com', password: 'secret' },
|
|
15
|
+
uat: { email: 'uat@test.com', password: 'uat-secret' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
modules: [
|
|
19
|
+
{ id: 'dashboard', route: '/dashboard', sidebarIcon: true },
|
|
20
|
+
{ id: 'invoices', route: '/invoices', sidebarIcon: true },
|
|
21
|
+
],
|
|
22
|
+
environments: {
|
|
23
|
+
alpha: { baseUrl: 'http://localhost:3000', seed: false },
|
|
24
|
+
uat: { baseUrl: 'http://uat.localhost:3000', seed: false },
|
|
25
|
+
},
|
|
26
|
+
portals: [],
|
|
27
|
+
breakpoints: [1280],
|
|
28
|
+
integrations: [],
|
|
29
|
+
workers: [],
|
|
30
|
+
workflows: [],
|
|
31
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
32
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
33
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
34
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
35
|
+
thresholds: {
|
|
36
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
37
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
38
|
+
apiResponseTime: 2000,
|
|
39
|
+
bundleSizeLimit: 500000,
|
|
40
|
+
maxConsoleErrors: 0,
|
|
41
|
+
},
|
|
42
|
+
disabledAgents: [],
|
|
43
|
+
disabledAgentSet: new Set<number>(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Mock child_process
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const spawnMock = vi.fn();
|
|
51
|
+
vi.mock('child_process', () => ({
|
|
52
|
+
execFile: (...args: unknown[]) => spawnMock(...args),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Mock path (used for video/snapshot paths — let it pass through)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Mock fetch (preFlight isUrlReachable + ApiClient)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const fetchMock = vi.fn();
|
|
64
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Mock helpers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const loginMock = vi.fn().mockResolvedValue(undefined);
|
|
71
|
+
const navigateToSectionMock = vi.fn().mockResolvedValue(undefined);
|
|
72
|
+
const waitForSectionLoadMock = vi.fn().mockResolvedValue(undefined);
|
|
73
|
+
const switchPortalMock = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
const captureScreenshotMock = vi.fn().mockResolvedValue('/tmp/test-run/screenshot.png');
|
|
75
|
+
const discoverInteractiveElementsMock = vi.fn().mockResolvedValue([]);
|
|
76
|
+
const fillFormMock = vi.fn().mockResolvedValue(undefined);
|
|
77
|
+
const openModalMock = vi.fn().mockResolvedValue(false);
|
|
78
|
+
const closeModalMock = vi.fn().mockResolvedValue(true);
|
|
79
|
+
|
|
80
|
+
vi.mock('../../helpers/navigation', () => ({
|
|
81
|
+
login: (...args: unknown[]) => loginMock(...args),
|
|
82
|
+
navigateToSection: (...args: unknown[]) => navigateToSectionMock(...args),
|
|
83
|
+
waitForSectionLoad: (...args: unknown[]) => waitForSectionLoadMock(...args),
|
|
84
|
+
switchPortal: (...args: unknown[]) => switchPortalMock(...args),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
vi.mock('../../helpers/screenshot', () => ({
|
|
88
|
+
captureScreenshot: (...args: unknown[]) => captureScreenshotMock(...args),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
vi.mock('../../helpers/element-discovery', () => ({
|
|
92
|
+
discoverInteractiveElements: (...args: unknown[]) => discoverInteractiveElementsMock(...args),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
vi.mock('../../helpers/form-filler', () => ({
|
|
96
|
+
fillForm: (...args: unknown[]) => fillFormMock(...args),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
vi.mock('../../helpers/modal-handler', () => ({
|
|
100
|
+
openModal: (...args: unknown[]) => openModalMock(...args),
|
|
101
|
+
closeModal: (...args: unknown[]) => closeModalMock(...args),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Mock ApiClient
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const apiClientGetMock = vi.fn();
|
|
109
|
+
const apiClientPutMock = vi.fn();
|
|
110
|
+
const createAuthenticatedMock = vi.fn();
|
|
111
|
+
|
|
112
|
+
vi.mock('../../helpers/api-client', () => ({
|
|
113
|
+
ApiClient: class MockApiClient {
|
|
114
|
+
static createAuthenticated(...args: unknown[]) {
|
|
115
|
+
return createAuthenticatedMock(...args);
|
|
116
|
+
}
|
|
117
|
+
get(...args: unknown[]) {
|
|
118
|
+
return apiClientGetMock(...args);
|
|
119
|
+
}
|
|
120
|
+
put(...args: unknown[]) {
|
|
121
|
+
return apiClientPutMock(...args);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Mock Playwright
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
type ConsoleListener = (msg: { type(): string; text(): string }) => void;
|
|
131
|
+
|
|
132
|
+
interface MockPage {
|
|
133
|
+
close: ReturnType<typeof vi.fn>;
|
|
134
|
+
on: ReturnType<typeof vi.fn>;
|
|
135
|
+
removeListener: ReturnType<typeof vi.fn>;
|
|
136
|
+
waitForTimeout: ReturnType<typeof vi.fn>;
|
|
137
|
+
setViewportSize: ReturnType<typeof vi.fn>;
|
|
138
|
+
goto: ReturnType<typeof vi.fn>;
|
|
139
|
+
_consoleListeners: ConsoleListener[];
|
|
140
|
+
emitConsole(type: string, text: string): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function makeMockPage(): MockPage {
|
|
144
|
+
const consoleListeners: ConsoleListener[] = [];
|
|
145
|
+
|
|
146
|
+
const page: MockPage = {
|
|
147
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
148
|
+
on: vi.fn().mockImplementation((event: string, listener: ConsoleListener) => {
|
|
149
|
+
if (event === 'console') consoleListeners.push(listener);
|
|
150
|
+
}),
|
|
151
|
+
removeListener: vi.fn().mockImplementation((event: string, listener: ConsoleListener) => {
|
|
152
|
+
if (event === 'console') {
|
|
153
|
+
const idx = consoleListeners.indexOf(listener);
|
|
154
|
+
if (idx !== -1) consoleListeners.splice(idx, 1);
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
waitForTimeout: vi.fn().mockResolvedValue(undefined),
|
|
158
|
+
setViewportSize: vi.fn().mockResolvedValue(undefined),
|
|
159
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
160
|
+
_consoleListeners: consoleListeners,
|
|
161
|
+
emitConsole(type: string, text: string): void {
|
|
162
|
+
for (const l of consoleListeners) {
|
|
163
|
+
l({ type: () => type, text: () => text });
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return page;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let currentMockPage: MockPage;
|
|
172
|
+
let mockBrowser: { newPage: ReturnType<typeof vi.fn>; close: ReturnType<typeof vi.fn> };
|
|
173
|
+
|
|
174
|
+
vi.mock('playwright', async () => ({
|
|
175
|
+
chromium: {
|
|
176
|
+
launch: vi.fn().mockImplementation(async () => mockBrowser),
|
|
177
|
+
},
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Dynamic import AFTER mocks are hoisted
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
const { BrowserCrawlerAgent } = await import('../05-browser-crawler');
|
|
185
|
+
|
|
186
|
+
function createUatAgent(
|
|
187
|
+
configOverride: Partial<ValidatedConfig> = {},
|
|
188
|
+
): InstanceType<typeof BrowserCrawlerAgent> {
|
|
189
|
+
return new BrowserCrawlerAgent(
|
|
190
|
+
{ ...stubConfig, ...configOverride },
|
|
191
|
+
'uat',
|
|
192
|
+
'/tmp/test-run',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Tests
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
describe('BrowserCrawlerAgent — UAT mode', () => {
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
spawnMock.mockReset();
|
|
203
|
+
fetchMock.mockReset();
|
|
204
|
+
loginMock.mockReset().mockResolvedValue(undefined);
|
|
205
|
+
navigateToSectionMock.mockReset().mockResolvedValue(undefined);
|
|
206
|
+
waitForSectionLoadMock.mockReset().mockResolvedValue(undefined);
|
|
207
|
+
switchPortalMock.mockReset().mockResolvedValue(undefined);
|
|
208
|
+
captureScreenshotMock.mockReset().mockResolvedValue('/tmp/test-run/screenshot.png');
|
|
209
|
+
discoverInteractiveElementsMock.mockReset().mockResolvedValue([]);
|
|
210
|
+
fillFormMock.mockReset().mockResolvedValue(undefined);
|
|
211
|
+
openModalMock.mockReset().mockResolvedValue(false);
|
|
212
|
+
closeModalMock.mockReset().mockResolvedValue(true);
|
|
213
|
+
apiClientGetMock.mockReset().mockResolvedValue({ status: 200, body: [] });
|
|
214
|
+
apiClientPutMock.mockReset().mockResolvedValue({ status: 200, body: {} });
|
|
215
|
+
createAuthenticatedMock.mockReset().mockResolvedValue({
|
|
216
|
+
get: apiClientGetMock,
|
|
217
|
+
put: apiClientPutMock,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
currentMockPage = makeMockPage();
|
|
221
|
+
mockBrowser = {
|
|
222
|
+
newPage: vi.fn().mockResolvedValue(currentMockPage),
|
|
223
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
spawnMock.mockImplementation(
|
|
227
|
+
(_file: string, _args: string[], _opts: unknown, cb: (err: Error | null, stdout: string, stderr: string) => void) => {
|
|
228
|
+
cb(null, '1.40.0\n', '');
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
// Basic sanity
|
|
236
|
+
// -------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
it('does not throw "not implemented" for UAT phase', async () => {
|
|
239
|
+
const agent = createUatAgent();
|
|
240
|
+
const result = await agent.run();
|
|
241
|
+
|
|
242
|
+
expect(result.findings.some(f => f.description.includes('not implemented'))).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('returns passed status when no errors occur and no destructive elements', async () => {
|
|
246
|
+
const agent = createUatAgent();
|
|
247
|
+
const result = await agent.run();
|
|
248
|
+
|
|
249
|
+
expect(result.status).toBe('passed');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// -------------------------------------------------------------------------
|
|
253
|
+
// UAT credentials
|
|
254
|
+
// -------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
it('uses UAT credentials (uat entry) when available', async () => {
|
|
257
|
+
const agent = createUatAgent();
|
|
258
|
+
await agent.run();
|
|
259
|
+
|
|
260
|
+
// login is called with UAT credentials
|
|
261
|
+
expect(loginMock).toHaveBeenCalledWith(
|
|
262
|
+
currentMockPage,
|
|
263
|
+
{ email: 'uat@test.com', password: 'uat-secret' },
|
|
264
|
+
expect.any(String),
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('falls back to admin credentials when uat entry is absent', async () => {
|
|
269
|
+
const configNoUat: Partial<ValidatedConfig> = {
|
|
270
|
+
auth: {
|
|
271
|
+
loginUrl: 'http://localhost/login',
|
|
272
|
+
credentials: {
|
|
273
|
+
admin: { email: 'admin@test.com', password: 'secret' },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
const agent = createUatAgent(configNoUat);
|
|
278
|
+
await agent.run();
|
|
279
|
+
|
|
280
|
+
expect(loginMock).toHaveBeenCalledWith(
|
|
281
|
+
currentMockPage,
|
|
282
|
+
{ email: 'admin@test.com', password: 'secret' },
|
|
283
|
+
expect.any(String),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// -------------------------------------------------------------------------
|
|
288
|
+
// Video evidence
|
|
289
|
+
// -------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
it('records a video evidence item', async () => {
|
|
292
|
+
const agent = createUatAgent();
|
|
293
|
+
const result = await agent.run();
|
|
294
|
+
|
|
295
|
+
const videoEvidence = result.evidence.filter(e => e.type === 'video');
|
|
296
|
+
expect(videoEvidence.length).toBeGreaterThanOrEqual(1);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// -------------------------------------------------------------------------
|
|
300
|
+
// Edge cases
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
it('calls fillForm with boundary inputs (empty, unicode, emoji, max-length)', async () => {
|
|
304
|
+
const agent = createUatAgent();
|
|
305
|
+
await agent.run();
|
|
306
|
+
|
|
307
|
+
const callValues = fillFormMock.mock.calls.map((c: unknown[]) => c[2]) as Array<Record<string, string> | undefined>;
|
|
308
|
+
expect(callValues).toContainEqual({ value: '' });
|
|
309
|
+
expect(callValues).toContainEqual({ value: '日本語テキスト' });
|
|
310
|
+
expect(callValues).toContainEqual({ value: '🎉🔥💡' });
|
|
311
|
+
expect(callValues.some((v) => v && typeof v === 'object' && 'value' in v && (v as Record<string, string>).value.length >= 1_000)).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// -------------------------------------------------------------------------
|
|
315
|
+
// Destructive snapshot / restore pattern
|
|
316
|
+
// -------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
it('takes a snapshot via API before executing a destructive action', async () => {
|
|
319
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
320
|
+
{
|
|
321
|
+
selector: '[data-testid="delete-btn"]',
|
|
322
|
+
type: 'button',
|
|
323
|
+
text: 'Delete',
|
|
324
|
+
isDestructive: true,
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
const agent = createUatAgent();
|
|
329
|
+
await agent.run();
|
|
330
|
+
|
|
331
|
+
// apiClientGet should have been called with the module resource path
|
|
332
|
+
expect(apiClientGetMock).toHaveBeenCalledWith(expect.stringMatching(/^\/api\//));
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('adds a snapshot evidence item before destructive action', async () => {
|
|
336
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
337
|
+
{
|
|
338
|
+
selector: '[data-testid="delete-btn"]',
|
|
339
|
+
type: 'button',
|
|
340
|
+
text: 'Delete',
|
|
341
|
+
isDestructive: true,
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
const agent = createUatAgent();
|
|
346
|
+
const result = await agent.run();
|
|
347
|
+
|
|
348
|
+
const snapshotEvidence = result.evidence.filter(e => e.type === 'snapshot');
|
|
349
|
+
expect(snapshotEvidence.length).toBeGreaterThanOrEqual(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('restores entity via API after destructive action', async () => {
|
|
353
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
354
|
+
{
|
|
355
|
+
selector: '[data-testid="delete-btn"]',
|
|
356
|
+
type: 'button',
|
|
357
|
+
text: 'Delete',
|
|
358
|
+
isDestructive: true,
|
|
359
|
+
},
|
|
360
|
+
]);
|
|
361
|
+
apiClientGetMock.mockResolvedValue({ status: 200, body: { id: 'entity-1' } });
|
|
362
|
+
|
|
363
|
+
const agent = createUatAgent();
|
|
364
|
+
await agent.run();
|
|
365
|
+
|
|
366
|
+
expect(apiClientPutMock).toHaveBeenCalledWith(
|
|
367
|
+
expect.stringMatching(/restore/),
|
|
368
|
+
expect.objectContaining({ snapshot: expect.anything() }),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// Restore failure → critical finding + stop destructive tests
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
it('creates a critical finding when restore fails', async () => {
|
|
377
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
378
|
+
{
|
|
379
|
+
selector: '[data-testid="delete-btn"]',
|
|
380
|
+
type: 'button',
|
|
381
|
+
text: 'Delete',
|
|
382
|
+
isDestructive: true,
|
|
383
|
+
},
|
|
384
|
+
]);
|
|
385
|
+
apiClientGetMock.mockResolvedValue({ status: 200, body: { id: 'entity-1' } });
|
|
386
|
+
apiClientPutMock.mockRejectedValue(new Error('Restore endpoint unavailable'));
|
|
387
|
+
|
|
388
|
+
const agent = createUatAgent();
|
|
389
|
+
const result = await agent.run();
|
|
390
|
+
|
|
391
|
+
const criticalFinding = result.findings.find(
|
|
392
|
+
f => f.severity === 'critical' && f.description.includes('Restore failed'),
|
|
393
|
+
);
|
|
394
|
+
expect(criticalFinding).toBeDefined();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('stops destructive tests after a restore failure', async () => {
|
|
398
|
+
const modules = [
|
|
399
|
+
{ id: 'dashboard', route: '/dashboard', sidebarIcon: true },
|
|
400
|
+
{ id: 'invoices', route: '/invoices', sidebarIcon: true },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
// Both modules have destructive elements
|
|
404
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
405
|
+
{
|
|
406
|
+
selector: '[data-testid="delete-btn"]',
|
|
407
|
+
type: 'button',
|
|
408
|
+
text: 'Delete',
|
|
409
|
+
isDestructive: true,
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
412
|
+
apiClientGetMock.mockResolvedValue({ status: 200, body: { id: 'entity-1' } });
|
|
413
|
+
// Restore always fails
|
|
414
|
+
apiClientPutMock.mockRejectedValue(new Error('Restore endpoint unavailable'));
|
|
415
|
+
|
|
416
|
+
const agent = createUatAgent({ modules });
|
|
417
|
+
await agent.run();
|
|
418
|
+
|
|
419
|
+
// After the first module's restore fails, stopDestructive=true;
|
|
420
|
+
// apiClientGet should be called only once for destructive pass (first module only)
|
|
421
|
+
const destructiveGetCalls = apiClientGetMock.mock.calls.filter(
|
|
422
|
+
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).startsWith('/api/'),
|
|
423
|
+
);
|
|
424
|
+
expect(destructiveGetCalls.length).toBe(1);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// -------------------------------------------------------------------------
|
|
428
|
+
// Missing env / credentials
|
|
429
|
+
// -------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
it('creates critical finding when no environment is configured', async () => {
|
|
432
|
+
const agent = createUatAgent({ environments: {} });
|
|
433
|
+
const result = await agent.run();
|
|
434
|
+
|
|
435
|
+
expect(result.status).toBe('failed');
|
|
436
|
+
expect(result.findings.some(f => f.severity === 'critical')).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('creates critical finding when no credentials are configured', async () => {
|
|
440
|
+
const agent = createUatAgent({
|
|
441
|
+
auth: { loginUrl: 'http://localhost/login', credentials: {} },
|
|
442
|
+
});
|
|
443
|
+
const result = await agent.run();
|
|
444
|
+
|
|
445
|
+
expect(result.status).toBe('failed');
|
|
446
|
+
expect(result.findings.some(f => f.severity === 'critical')).toBe(true);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// -------------------------------------------------------------------------
|
|
450
|
+
// UAT: login failure produces finding with fallback severity
|
|
451
|
+
// -------------------------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
it('creates a login failure finding when login throws', async () => {
|
|
454
|
+
loginMock.mockRejectedValue(new Error('Auth service down'));
|
|
455
|
+
|
|
456
|
+
const agent = createUatAgent();
|
|
457
|
+
const result = await agent.run();
|
|
458
|
+
|
|
459
|
+
const loginFinding = result.findings.find(f => f.id === '5-uat-login-failed');
|
|
460
|
+
expect(loginFinding).toBeDefined();
|
|
461
|
+
expect(loginFinding!.description).toContain('Auth service down');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// -------------------------------------------------------------------------
|
|
465
|
+
// UAT: edge-case console errors
|
|
466
|
+
// -------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
it('creates a finding for console errors during edge-case pass', async () => {
|
|
469
|
+
navigateToSectionMock.mockImplementation(
|
|
470
|
+
async (_page: unknown, moduleId: string) => {
|
|
471
|
+
if (moduleId === 'dashboard') {
|
|
472
|
+
currentMockPage.emitConsole('error', 'TypeError: Cannot read properties of null');
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const agent = createUatAgent();
|
|
478
|
+
const result = await agent.run();
|
|
479
|
+
|
|
480
|
+
const edgeFinding = result.findings.find(f =>
|
|
481
|
+
f.id.includes('uat-edge') && f.consoleError !== undefined,
|
|
482
|
+
);
|
|
483
|
+
expect(edgeFinding).toBeDefined();
|
|
484
|
+
expect(edgeFinding!.severity).toBe('medium');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
// UAT: edge-case navigation failure is silently skipped
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
it('skips modules where navigation fails during edge-case pass', async () => {
|
|
492
|
+
// Only fail edge-case navigation by failing the second time navigation is called per module
|
|
493
|
+
let navCallCount = 0;
|
|
494
|
+
navigateToSectionMock.mockImplementation(async () => {
|
|
495
|
+
navCallCount++;
|
|
496
|
+
// Fail on every call to force edge-case skip path
|
|
497
|
+
if (navCallCount > 2) throw new Error('Nav fail');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const agent = createUatAgent();
|
|
501
|
+
const result = await agent.run();
|
|
502
|
+
|
|
503
|
+
// Should still complete without crashing
|
|
504
|
+
expect(result.status).toBe('passed');
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// -------------------------------------------------------------------------
|
|
508
|
+
// UAT: destructive action error creates a test-bug finding
|
|
509
|
+
// -------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
it('creates a test-bug finding when destructive action throws', async () => {
|
|
512
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
513
|
+
{
|
|
514
|
+
selector: '[data-testid="archive-btn"]',
|
|
515
|
+
type: 'button',
|
|
516
|
+
text: 'Archive',
|
|
517
|
+
isDestructive: true,
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
openModalMock.mockRejectedValue(new Error('Button detached'));
|
|
521
|
+
apiClientGetMock.mockResolvedValue({ status: 200, body: { id: 'e1' } });
|
|
522
|
+
|
|
523
|
+
const agent = createUatAgent();
|
|
524
|
+
const result = await agent.run();
|
|
525
|
+
|
|
526
|
+
const actionFinding = result.findings.find(f =>
|
|
527
|
+
f.type === 'test-bug' && f.description.includes('Button detached'),
|
|
528
|
+
);
|
|
529
|
+
expect(actionFinding).toBeDefined();
|
|
530
|
+
expect(actionFinding!.severity).toBe('medium');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// -------------------------------------------------------------------------
|
|
534
|
+
// UAT: destructive action with link element type
|
|
535
|
+
// -------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
it('handles link-type destructive elements via openModal', async () => {
|
|
538
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
539
|
+
{
|
|
540
|
+
selector: 'a[data-testid="delete-link"]',
|
|
541
|
+
type: 'link',
|
|
542
|
+
text: 'Delete',
|
|
543
|
+
isDestructive: true,
|
|
544
|
+
},
|
|
545
|
+
]);
|
|
546
|
+
openModalMock.mockResolvedValue(true);
|
|
547
|
+
apiClientGetMock.mockResolvedValue({ status: 200, body: { id: 'e1' } });
|
|
548
|
+
|
|
549
|
+
const agent = createUatAgent();
|
|
550
|
+
await agent.run();
|
|
551
|
+
|
|
552
|
+
expect(openModalMock).toHaveBeenCalled();
|
|
553
|
+
expect(closeModalMock).toHaveBeenCalledWith(currentMockPage, 'escape');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// -------------------------------------------------------------------------
|
|
557
|
+
// UAT: no apiClient available — snapshot is null
|
|
558
|
+
// -------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
it('handles null apiClient gracefully for destructive pass', async () => {
|
|
561
|
+
createAuthenticatedMock.mockRejectedValue(new Error('No API'));
|
|
562
|
+
discoverInteractiveElementsMock.mockResolvedValue([
|
|
563
|
+
{
|
|
564
|
+
selector: '[data-testid="del"]',
|
|
565
|
+
type: 'button',
|
|
566
|
+
text: 'Del',
|
|
567
|
+
isDestructive: true,
|
|
568
|
+
},
|
|
569
|
+
]);
|
|
570
|
+
|
|
571
|
+
const agent = createUatAgent();
|
|
572
|
+
const result = await agent.run();
|
|
573
|
+
|
|
574
|
+
// Should not crash, and should not attempt restore (no apiClient)
|
|
575
|
+
expect(apiClientPutMock).not.toHaveBeenCalled();
|
|
576
|
+
expect(result.status).toBe('passed');
|
|
577
|
+
});
|
|
578
|
+
});
|