@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,117 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateCodeQuality } from '../quality-gate';
|
|
3
|
+
import type { QualityViolation } from '../quality-gate';
|
|
4
|
+
|
|
5
|
+
describe('validateCodeQuality(fileContent, filePath)', () => {
|
|
6
|
+
describe('any type detection', () => {
|
|
7
|
+
it('reports a violation when `any` type is used', () => {
|
|
8
|
+
const content = `const x: any = 5;`;
|
|
9
|
+
const violations: QualityViolation[] = validateCodeQuality(content, 'test.ts');
|
|
10
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
11
|
+
const v = violations.find((v) => v.rule === 'no-any');
|
|
12
|
+
expect(v).toBeDefined();
|
|
13
|
+
expect(v?.message).toMatch(/any/i);
|
|
14
|
+
expect(v?.severity).toBe('error');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('reports no violation for a typed variable without `any`', () => {
|
|
18
|
+
const content = `const x: string = 'hello';`;
|
|
19
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
20
|
+
const anyViolations = violations.filter((v) => v.rule === 'no-any');
|
|
21
|
+
expect(anyViolations).toHaveLength(0);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('long function detection (>30 lines)', () => {
|
|
26
|
+
it('reports a violation for a function body with 35 lines', () => {
|
|
27
|
+
const bodyLines = Array.from({ length: 35 }, (_, i) => ` const v${i} = ${i};`);
|
|
28
|
+
const content = `function longFn() {\n${bodyLines.join('\n')}\n}`;
|
|
29
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
30
|
+
const v = violations.find((v) => v.rule === 'max-function-lines');
|
|
31
|
+
expect(v).toBeDefined();
|
|
32
|
+
expect(v?.severity).toBe('warning');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reports no violation for a function body with 10 lines', () => {
|
|
36
|
+
const bodyLines = Array.from({ length: 10 }, (_, i) => ` const v${i} = ${i};`);
|
|
37
|
+
const content = `function shortFn() {\n${bodyLines.join('\n')}\n}`;
|
|
38
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
39
|
+
const fnViolations = violations.filter((v) => v.rule === 'max-function-lines');
|
|
40
|
+
expect(fnViolations).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('hardcoded secret detection', () => {
|
|
45
|
+
it('reports a violation for a hardcoded password assignment', () => {
|
|
46
|
+
const content = `const password = 'secret123';`;
|
|
47
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
48
|
+
const v = violations.find((v) => v.rule === 'no-hardcoded-secrets');
|
|
49
|
+
expect(v).toBeDefined();
|
|
50
|
+
expect(v?.severity).toBe('error');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reports no violation when password uses process.env', () => {
|
|
54
|
+
const content = `const password = process.env.PASSWORD;`;
|
|
55
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
56
|
+
const secretViolations = violations.filter((v) => v.rule === 'no-hardcoded-secrets');
|
|
57
|
+
expect(secretViolations).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('reports a violation for a hardcoded apiKey assignment', () => {
|
|
61
|
+
const content = `const apiKey = 'sk-1234567890';`;
|
|
62
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
63
|
+
const v = violations.find((v) => v.rule === 'no-hardcoded-secrets');
|
|
64
|
+
expect(v).toBeDefined();
|
|
65
|
+
expect(v?.severity).toBe('error');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('deep nesting detection (>3 levels)', () => {
|
|
70
|
+
it('reports a violation for 4 nested if blocks', () => {
|
|
71
|
+
const content = [
|
|
72
|
+
`if (a) {`,
|
|
73
|
+
` if (b) {`,
|
|
74
|
+
` if (c) {`,
|
|
75
|
+
` if (d) {`,
|
|
76
|
+
` doSomething();`,
|
|
77
|
+
` }`,
|
|
78
|
+
` }`,
|
|
79
|
+
` }`,
|
|
80
|
+
`}`,
|
|
81
|
+
].join('\n');
|
|
82
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
83
|
+
const v = violations.find((v) => v.rule === 'max-nesting-depth');
|
|
84
|
+
expect(v).toBeDefined();
|
|
85
|
+
expect(v?.severity).toBe('warning');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reports no violation for 2 nested if blocks', () => {
|
|
89
|
+
const content = [
|
|
90
|
+
`if (a) {`,
|
|
91
|
+
` if (b) {`,
|
|
92
|
+
` doSomething();`,
|
|
93
|
+
` }`,
|
|
94
|
+
`}`,
|
|
95
|
+
].join('\n');
|
|
96
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
97
|
+
const nestingViolations = violations.filter((v) => v.rule === 'max-nesting-depth');
|
|
98
|
+
expect(nestingViolations).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('QualityViolation shape', () => {
|
|
103
|
+
it('each violation has rule, message, severity, and optional line', () => {
|
|
104
|
+
const content = `const x: any = 5;`;
|
|
105
|
+
const violations = validateCodeQuality(content, 'test.ts');
|
|
106
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
107
|
+
for (const v of violations) {
|
|
108
|
+
expect(typeof v.rule).toBe('string');
|
|
109
|
+
expect(typeof v.message).toBe('string');
|
|
110
|
+
expect(['error', 'warning']).toContain(v.severity);
|
|
111
|
+
if (v.line !== undefined) {
|
|
112
|
+
expect(typeof v.line).toBe('number');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { PNG } from 'pngjs';
|
|
5
|
+
import { compareScreenshots, updateBaseline } from '../screenshot';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const TMP_DIR = join('/tmp', 'tf-screenshot-tests');
|
|
12
|
+
|
|
13
|
+
function createSolidPng(filePath: string, r: number, g: number, b: number): void {
|
|
14
|
+
const width = 10;
|
|
15
|
+
const height = 10;
|
|
16
|
+
const png = new PNG({ width, height });
|
|
17
|
+
|
|
18
|
+
for (let y = 0; y < height; y++) {
|
|
19
|
+
for (let x = 0; x < width; x++) {
|
|
20
|
+
const idx = (y * width + x) * 4;
|
|
21
|
+
png.data[idx] = r;
|
|
22
|
+
png.data[idx + 1] = g;
|
|
23
|
+
png.data[idx + 2] = b;
|
|
24
|
+
png.data[idx + 3] = 255;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeFileSync(filePath, PNG.sync.write(png));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Setup / teardown
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
if (!existsSync(TMP_DIR)) {
|
|
37
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
rmSync(TMP_DIR, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Tests
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
describe('compareScreenshots(currentPath, baselinePath, threshold?)', () => {
|
|
50
|
+
it('returns match=true and diffPercentage=0 for identical images', async () => {
|
|
51
|
+
const pathA = join(TMP_DIR, 'identical_a.png');
|
|
52
|
+
const pathB = join(TMP_DIR, 'identical_b.png');
|
|
53
|
+
createSolidPng(pathA, 255, 0, 0);
|
|
54
|
+
createSolidPng(pathB, 255, 0, 0);
|
|
55
|
+
|
|
56
|
+
const result = await compareScreenshots(pathA, pathB);
|
|
57
|
+
|
|
58
|
+
expect(result.match).toBe(true);
|
|
59
|
+
expect(result.diffPercentage).toBe(0);
|
|
60
|
+
expect(result.diffPath).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns match=false and diffPercentage>0 for completely different images', async () => {
|
|
64
|
+
const current = join(TMP_DIR, 'diff_current.png');
|
|
65
|
+
const baseline = join(TMP_DIR, 'diff_baseline.png');
|
|
66
|
+
createSolidPng(current, 255, 0, 0); // red
|
|
67
|
+
createSolidPng(baseline, 0, 0, 255); // blue
|
|
68
|
+
|
|
69
|
+
const result = await compareScreenshots(current, baseline);
|
|
70
|
+
|
|
71
|
+
expect(result.match).toBe(false);
|
|
72
|
+
expect(result.diffPercentage).toBeGreaterThan(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('creates a diff PNG file when images differ', async () => {
|
|
76
|
+
const current = join(TMP_DIR, 'diff_file_current.png');
|
|
77
|
+
const baseline = join(TMP_DIR, 'diff_file_baseline.png');
|
|
78
|
+
createSolidPng(current, 255, 0, 0);
|
|
79
|
+
createSolidPng(baseline, 0, 255, 0);
|
|
80
|
+
|
|
81
|
+
const result = await compareScreenshots(current, baseline);
|
|
82
|
+
|
|
83
|
+
expect(result.match).toBe(false);
|
|
84
|
+
expect(result.diffPath).toBeDefined();
|
|
85
|
+
expect(existsSync(result.diffPath!)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns match=false for images with different dimensions', async () => {
|
|
89
|
+
const small = new PNG({ width: 5, height: 5 });
|
|
90
|
+
small.data.fill(255);
|
|
91
|
+
writeFileSync(join(TMP_DIR, 'small.png'), PNG.sync.write(small));
|
|
92
|
+
|
|
93
|
+
const large = new PNG({ width: 10, height: 10 });
|
|
94
|
+
large.data.fill(255);
|
|
95
|
+
writeFileSync(join(TMP_DIR, 'large.png'), PNG.sync.write(large));
|
|
96
|
+
|
|
97
|
+
const result = await compareScreenshots(
|
|
98
|
+
join(TMP_DIR, 'small.png'),
|
|
99
|
+
join(TMP_DIR, 'large.png'),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.match).toBe(false);
|
|
103
|
+
expect(result.diffPercentage).toBe(100);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('respects a custom threshold — identical images pass any positive threshold', async () => {
|
|
107
|
+
const pathA = join(TMP_DIR, 'thresh_a.png');
|
|
108
|
+
const pathB = join(TMP_DIR, 'thresh_b.png');
|
|
109
|
+
createSolidPng(pathA, 128, 128, 128);
|
|
110
|
+
createSolidPng(pathB, 128, 128, 128);
|
|
111
|
+
|
|
112
|
+
const result = await compareScreenshots(pathA, pathB, 0.5);
|
|
113
|
+
|
|
114
|
+
expect(result.match).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('updateBaseline(currentPath, baselineDir)', () => {
|
|
119
|
+
it('copies the current screenshot to the baseline directory', async () => {
|
|
120
|
+
const current = join(TMP_DIR, 'to_baseline.png');
|
|
121
|
+
const baselineDir = join(TMP_DIR, 'baselines');
|
|
122
|
+
createSolidPng(current, 0, 128, 0);
|
|
123
|
+
|
|
124
|
+
const resultPath = await updateBaseline(current, baselineDir);
|
|
125
|
+
|
|
126
|
+
expect(existsSync(resultPath)).toBe(true);
|
|
127
|
+
expect(resultPath).toContain('to_baseline.png');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('creates the baseline directory if it does not exist', async () => {
|
|
131
|
+
const current = join(TMP_DIR, 'new_dir_test.png');
|
|
132
|
+
const baselineDir = join(TMP_DIR, 'new_baselines_dir');
|
|
133
|
+
createSolidPng(current, 0, 0, 128);
|
|
134
|
+
|
|
135
|
+
await updateBaseline(current, baselineDir);
|
|
136
|
+
|
|
137
|
+
expect(existsSync(baselineDir)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { validateSeedCompleteness } from '../seed-validator';
|
|
5
|
+
|
|
6
|
+
const TMP_DIR = join('/tmp', 'tf-seed-validator-tests');
|
|
7
|
+
|
|
8
|
+
const SEED_CONTENT = `
|
|
9
|
+
import { PrismaClient } from '@prisma/client';
|
|
10
|
+
const prisma = new PrismaClient();
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
// Seed users
|
|
14
|
+
await prisma.user.createMany({
|
|
15
|
+
data: [
|
|
16
|
+
{ email: 'admin@example.com', role: 'ADMIN' },
|
|
17
|
+
{ email: 'staff@example.com', role: 'STAFF' },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Seed clients
|
|
22
|
+
await prisma.client.create({
|
|
23
|
+
data: { name: 'ACME Corp', abn: '12345678901' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Seed invoices
|
|
27
|
+
await prisma.invoice.upsert({
|
|
28
|
+
where: { id: 1 },
|
|
29
|
+
update: {},
|
|
30
|
+
create: { clientId: 1, amount: 1000 },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Insert raw transactions
|
|
34
|
+
await prisma.$executeRaw\`INSERT INTO transactions (amount) VALUES (500)\`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main();
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
if (!existsSync(TMP_DIR)) {
|
|
42
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(join(TMP_DIR, 'seed.ts'), SEED_CONTENT, 'utf-8');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
rmSync(TMP_DIR, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('validateSeedCompleteness(seedFilePath, moduleIds)', () => {
|
|
52
|
+
it('detects covered modules that appear near creation keywords', async () => {
|
|
53
|
+
const result = await validateSeedCompleteness(
|
|
54
|
+
join(TMP_DIR, 'seed.ts'),
|
|
55
|
+
['user', 'client', 'invoice'],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result.covered).toContain('user');
|
|
59
|
+
expect(result.covered).toContain('client');
|
|
60
|
+
expect(result.covered).toContain('invoice');
|
|
61
|
+
expect(result.missing).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('identifies missing modules not present in the seed file', async () => {
|
|
65
|
+
const result = await validateSeedCompleteness(
|
|
66
|
+
join(TMP_DIR, 'seed.ts'),
|
|
67
|
+
['user', 'client', 'invoice', 'payment', 'subscription'],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result.covered).toContain('user');
|
|
71
|
+
expect(result.covered).toContain('client');
|
|
72
|
+
expect(result.missing).toContain('payment');
|
|
73
|
+
expect(result.missing).toContain('subscription');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns all modules as missing when seed file is empty', async () => {
|
|
77
|
+
const emptyPath = join(TMP_DIR, 'empty-seed.ts');
|
|
78
|
+
writeFileSync(emptyPath, '', 'utf-8');
|
|
79
|
+
|
|
80
|
+
const result = await validateSeedCompleteness(emptyPath, ['user', 'client']);
|
|
81
|
+
|
|
82
|
+
expect(result.covered).toHaveLength(0);
|
|
83
|
+
expect(result.missing).toContain('user');
|
|
84
|
+
expect(result.missing).toContain('client');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns all modules as covered when every module matches', async () => {
|
|
88
|
+
const result = await validateSeedCompleteness(
|
|
89
|
+
join(TMP_DIR, 'seed.ts'),
|
|
90
|
+
['user'],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(result.covered).toContain('user');
|
|
94
|
+
expect(result.missing).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('is case-insensitive for module names', async () => {
|
|
98
|
+
const result = await validateSeedCompleteness(
|
|
99
|
+
join(TMP_DIR, 'seed.ts'),
|
|
100
|
+
['User', 'CLIENT', 'Invoice'],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(result.covered).toContain('User');
|
|
104
|
+
expect(result.covered).toContain('CLIENT');
|
|
105
|
+
expect(result.covered).toContain('Invoice');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns empty covered and missing arrays when moduleIds is empty', async () => {
|
|
109
|
+
const result = await validateSeedCompleteness(join(TMP_DIR, 'seed.ts'), []);
|
|
110
|
+
|
|
111
|
+
expect(result.covered).toHaveLength(0);
|
|
112
|
+
expect(result.missing).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface ApiResponse {
|
|
2
|
+
status: number;
|
|
3
|
+
body: unknown;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface LoginResponseBody {
|
|
9
|
+
access_token?: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
accessToken?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lightweight HTTP client for integration and API tests.
|
|
16
|
+
* Automatically attaches Authorization headers when a token is present
|
|
17
|
+
* and records response duration for every request.
|
|
18
|
+
*/
|
|
19
|
+
export class ApiClient {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly baseUrl: string,
|
|
22
|
+
private token?: string,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an authenticated ApiClient by POSTing credentials to /api/auth/login
|
|
27
|
+
* and storing the returned JWT for subsequent requests.
|
|
28
|
+
*/
|
|
29
|
+
static async createAuthenticated(
|
|
30
|
+
baseUrl: string,
|
|
31
|
+
credentials: { email: string; password: string },
|
|
32
|
+
): Promise<ApiClient> {
|
|
33
|
+
const client = new ApiClient(baseUrl);
|
|
34
|
+
const response = await client.post('/api/auth/login', credentials);
|
|
35
|
+
|
|
36
|
+
const body = response.body as LoginResponseBody;
|
|
37
|
+
const jwt = body?.access_token ?? body?.accessToken ?? body?.token;
|
|
38
|
+
|
|
39
|
+
if (!jwt) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Authentication failed: no token in response (status ${response.status})`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new ApiClient(baseUrl, String(jwt));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async get(path: string): Promise<ApiResponse> {
|
|
49
|
+
return this.request('GET', path);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async post(path: string, body?: unknown): Promise<ApiResponse> {
|
|
53
|
+
return this.request('POST', path, body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async put(path: string, body?: unknown): Promise<ApiResponse> {
|
|
57
|
+
return this.request('PUT', path, body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async delete(path: string): Promise<ApiResponse> {
|
|
61
|
+
return this.request('DELETE', path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async request(
|
|
65
|
+
method: string,
|
|
66
|
+
path: string,
|
|
67
|
+
body?: unknown,
|
|
68
|
+
): Promise<ApiResponse> {
|
|
69
|
+
const url = `${this.baseUrl}${path}`;
|
|
70
|
+
const headers: Record<string, string> = {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (this.token) {
|
|
75
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const init: RequestInit = {
|
|
79
|
+
method,
|
|
80
|
+
headers,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (body !== undefined) {
|
|
84
|
+
init.body = JSON.stringify(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
const response = await fetch(url, init);
|
|
89
|
+
const durationMs = Date.now() - start;
|
|
90
|
+
|
|
91
|
+
const responseHeaders: Record<string, string> = {};
|
|
92
|
+
response.headers.forEach((value, key) => {
|
|
93
|
+
responseHeaders[key] = value;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let responseBody: unknown;
|
|
97
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
98
|
+
if (contentType.includes('application/json')) {
|
|
99
|
+
responseBody = await response.json();
|
|
100
|
+
} else {
|
|
101
|
+
responseBody = await response.text();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
status: response.status,
|
|
106
|
+
body: responseBody,
|
|
107
|
+
headers: responseHeaders,
|
|
108
|
+
durationMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
export interface DiscoveredElement {
|
|
4
|
+
selector: string;
|
|
5
|
+
type: 'button' | 'link' | 'input' | 'select' | 'textarea' | 'tab' | 'menuitem' | 'other';
|
|
6
|
+
text: string;
|
|
7
|
+
isDestructive: boolean;
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Keywords that indicate a destructive action
|
|
12
|
+
const DESTRUCTIVE_KEYWORDS = ['delete', 'remove', 'archive', 'destroy', 'clear', 'reset'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns true when the given text (lowercased) contains any destructive keyword.
|
|
16
|
+
*/
|
|
17
|
+
export function classifyAsDestructive(text: string): boolean {
|
|
18
|
+
const lower = text.toLowerCase();
|
|
19
|
+
return DESTRUCTIVE_KEYWORDS.some((keyword) => lower.includes(keyword));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// CSS selectors targeting all interactive element categories
|
|
23
|
+
const INTERACTIVE_SELECTORS = [
|
|
24
|
+
'button',
|
|
25
|
+
'a[href]',
|
|
26
|
+
'input',
|
|
27
|
+
'select',
|
|
28
|
+
'textarea',
|
|
29
|
+
'[role="button"]',
|
|
30
|
+
'[role="tab"]',
|
|
31
|
+
'[role="menuitem"]',
|
|
32
|
+
'[data-testid]',
|
|
33
|
+
'[aria-haspopup]',
|
|
34
|
+
].join(', ');
|
|
35
|
+
|
|
36
|
+
type ElementType = DiscoveredElement['type'];
|
|
37
|
+
|
|
38
|
+
function resolveType(tag: string, role: string): ElementType {
|
|
39
|
+
if (role === 'tab') return 'tab';
|
|
40
|
+
if (role === 'menuitem') return 'menuitem';
|
|
41
|
+
if (role === 'button' || tag === 'button') return 'button';
|
|
42
|
+
if (tag === 'a') return 'link';
|
|
43
|
+
if (tag === 'input') return 'input';
|
|
44
|
+
if (tag === 'select') return 'select';
|
|
45
|
+
if (tag === 'textarea') return 'textarea';
|
|
46
|
+
return 'other';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSelector(
|
|
50
|
+
testId: string | null,
|
|
51
|
+
ariaLabel: string | null,
|
|
52
|
+
tag: string,
|
|
53
|
+
text: string,
|
|
54
|
+
): string {
|
|
55
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
56
|
+
if (ariaLabel) return `[aria-label="${ariaLabel}"]`;
|
|
57
|
+
const safeSuffix = text.trim().slice(0, 40).replace(/"/g, '\\"');
|
|
58
|
+
return safeSuffix ? `${tag}:text("${safeSuffix}")` : tag;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Discovers all interactive elements on the given Playwright page and returns
|
|
63
|
+
* a deduplicated, classified list with unique selectors.
|
|
64
|
+
*/
|
|
65
|
+
export async function discoverInteractiveElements(page: Page): Promise<DiscoveredElement[]> {
|
|
66
|
+
type RawElement = {
|
|
67
|
+
tag: string;
|
|
68
|
+
role: string;
|
|
69
|
+
text: string;
|
|
70
|
+
ariaLabel: string | null;
|
|
71
|
+
testId: string | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const rawElements: RawElement[] = await page.evaluate((selector: string) => {
|
|
75
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
76
|
+
return nodes.map((el) => ({
|
|
77
|
+
tag: el.tagName.toLowerCase(),
|
|
78
|
+
role: el.getAttribute('role') ?? '',
|
|
79
|
+
text: (el.textContent ?? '').trim(),
|
|
80
|
+
ariaLabel: el.getAttribute('aria-label'),
|
|
81
|
+
testId: el.getAttribute('data-testid'),
|
|
82
|
+
}));
|
|
83
|
+
}, INTERACTIVE_SELECTORS);
|
|
84
|
+
|
|
85
|
+
const seenSelectors = new Set<string>();
|
|
86
|
+
const discovered: DiscoveredElement[] = [];
|
|
87
|
+
|
|
88
|
+
for (const raw of rawElements) {
|
|
89
|
+
const selector = buildSelector(raw.testId, raw.ariaLabel, raw.tag, raw.text);
|
|
90
|
+
if (seenSelectors.has(selector)) continue;
|
|
91
|
+
seenSelectors.add(selector);
|
|
92
|
+
|
|
93
|
+
const labelText = raw.ariaLabel ?? raw.text;
|
|
94
|
+
const discovered_element: DiscoveredElement = {
|
|
95
|
+
selector,
|
|
96
|
+
type: resolveType(raw.tag, raw.role),
|
|
97
|
+
text: raw.text,
|
|
98
|
+
isDestructive: classifyAsDestructive(labelText),
|
|
99
|
+
...(raw.ariaLabel !== null ? { ariaLabel: raw.ariaLabel } : {}),
|
|
100
|
+
};
|
|
101
|
+
discovered.push(discovered_element);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return discovered;
|
|
105
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ValidatedConfig } from '../core/config';
|
|
2
|
+
import type { Phase } from '../core/types';
|
|
3
|
+
|
|
4
|
+
interface EnvironmentConfig {
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
seed?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ResolvedEnvironment {
|
|
10
|
+
env: EnvironmentConfig;
|
|
11
|
+
/** True when the phase's configured URL was unreachable and we fell back to alpha. */
|
|
12
|
+
fallback: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check whether a URL responds with a non-5xx status within a timeout.
|
|
17
|
+
* Exported so agents can use it directly without duplicating the function.
|
|
18
|
+
*/
|
|
19
|
+
export async function isUrlReachable(url: string, timeoutMs = 5000): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
method: 'HEAD',
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
+
});
|
|
25
|
+
return response.ok || response.status < 500;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the environment for a given phase with automatic fallback.
|
|
33
|
+
*
|
|
34
|
+
* 1. Try the phase's configured environment (`config.environments[phase]`).
|
|
35
|
+
* 2. If the URL is reachable → return `{ env, fallback: false }`.
|
|
36
|
+
* 3. If unreachable → try `config.environments['alpha']` (localhost).
|
|
37
|
+
* 4. If alpha is reachable → return `{ env: alphaEnv, fallback: true }`.
|
|
38
|
+
* 5. If neither is reachable → return the phase's env with `fallback: false`
|
|
39
|
+
* so the agent can decide how to handle the failure.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveEnvironment(
|
|
42
|
+
config: ValidatedConfig,
|
|
43
|
+
phase: Phase,
|
|
44
|
+
): Promise<ResolvedEnvironment> {
|
|
45
|
+
// Phase-specific environment
|
|
46
|
+
const phaseEnv = config.environments[phase];
|
|
47
|
+
if (phaseEnv) {
|
|
48
|
+
const reachable = await isUrlReachable(phaseEnv.baseUrl);
|
|
49
|
+
if (reachable) {
|
|
50
|
+
return { env: phaseEnv, fallback: false };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback chain: alpha → first available
|
|
55
|
+
const alphaEnv = config.environments['alpha'];
|
|
56
|
+
if (alphaEnv && alphaEnv.baseUrl !== phaseEnv?.baseUrl) {
|
|
57
|
+
const reachable = await isUrlReachable(alphaEnv.baseUrl);
|
|
58
|
+
if (reachable) {
|
|
59
|
+
return { env: alphaEnv, fallback: true };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Nothing reachable — return phase env (or first available) and let agent handle it
|
|
64
|
+
const fallbackEnv = phaseEnv ?? Object.values(config.environments)[0];
|
|
65
|
+
if (!fallbackEnv) {
|
|
66
|
+
throw new Error(`No environment configured for phase "${phase}"`);
|
|
67
|
+
}
|
|
68
|
+
return { env: fallbackEnv, fallback: false };
|
|
69
|
+
}
|