@avi770/testteam 1.2.0 → 2.0.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 +29 -1
- package/README.md +53 -6
- package/agents/24-signup-onboarding-tester.ts +429 -0
- package/agents/25-crud-flow-tester.ts +302 -0
- package/agents/26-form-validator.ts +297 -0
- package/agents/27-search-filter-tester.ts +326 -0
- package/agents/28-navigation-routing-tester.ts +425 -0
- package/agents/29-responsive-interaction-tester.ts +350 -0
- package/agents/30-multi-user-scenario-tester.ts +319 -0
- package/agents/31-load-tester.ts +134 -0
- package/agents/32-memory-leak-detector.ts +194 -0
- package/agents/33-bundle-analyzer.ts +132 -0
- package/agents/34-xss-scanner.ts +191 -0
- package/agents/35-csrf-tester.ts +82 -0
- package/agents/36-auth-fuzzer.ts +194 -0
- package/agents/37-dependency-scanner.ts +176 -0
- package/agents/38-secrets-scanner.ts +137 -0
- package/agents/39-api-contract-tester.ts +199 -0
- package/agents/40-rate-limit-tester.ts +94 -0
- package/agents/41-api-pagination-tester.ts +97 -0
- package/agents/42-graphql-tester.ts +222 -0
- package/agents/43-data-consistency-checker.ts +205 -0
- package/agents/44-backup-recovery-tester.ts +152 -0
- package/agents/45-data-privacy-scanner.ts +125 -0
- package/agents/46-seo-auditor.ts +294 -0
- package/agents/47-social-preview-tester.ts +232 -0
- package/agents/48-lighthouse-auditor.ts +213 -0
- package/agents/49-i18n-tester.ts +198 -0
- package/agents/50-timezone-tester.ts +173 -0
- package/agents/51-error-recovery-tester.ts +155 -0
- package/agents/52-offline-mode-tester.ts +180 -0
- package/agents/53-graceful-degradation-tester.ts +156 -0
- package/agents/54-websocket-tester.ts +151 -0
- package/agents/55-realtime-sync-tester.ts +194 -0
- package/agents/56-file-upload-tester.ts +194 -0
- package/agents/57-export-tester.ts +174 -0
- package/agents/58-payment-flow-tester.ts +183 -0
- package/agents/59-ssl-tls-auditor.ts +141 -0
- package/agents/60-dns-cdn-tester.ts +117 -0
- package/agents/61-docker-health-checker.ts +111 -0
- package/agents/62-env-config-validator.ts +152 -0
- package/agents/63-log-quality-auditor.ts +136 -0
- package/agents/64-analytics-tracker-tester.ts +165 -0
- package/agents/65-gdpr-compliance-tester.ts +215 -0
- package/agents/66-soc2-control-validator.ts +210 -0
- package/agents/67-wcag-aaa-tester.ts +241 -0
- package/agents/68-dead-code-detector.ts +135 -0
- package/agents/69-type-safety-auditor.ts +164 -0
- package/agents/70-complexity-analyzer.ts +179 -0
- package/agents/__tests__/24-signup-onboarding-tester.test.ts +274 -0
- package/agents/__tests__/25-crud-flow-tester.test.ts +322 -0
- package/agents/__tests__/26-form-validator.test.ts +345 -0
- package/agents/__tests__/27-search-filter-tester.test.ts +311 -0
- package/agents/__tests__/28-navigation-routing-tester.test.ts +328 -0
- package/agents/__tests__/29-responsive-interaction-tester.test.ts +297 -0
- package/agents/__tests__/30-multi-user-scenario-tester.test.ts +328 -0
- package/agents/__tests__/31-load-tester.test.ts +189 -0
- package/agents/__tests__/32-memory-leak-detector.test.ts +251 -0
- package/agents/__tests__/33-bundle-analyzer.test.ts +237 -0
- package/agents/__tests__/34-xss-scanner.test.ts +258 -0
- package/agents/__tests__/35-csrf-tester.test.ts +200 -0
- package/agents/__tests__/36-auth-fuzzer.test.ts +214 -0
- package/agents/__tests__/37-dependency-scanner.test.ts +266 -0
- package/agents/__tests__/38-secrets-scanner.test.ts +224 -0
- package/agents/__tests__/39-api-contract-tester.test.ts +312 -0
- package/agents/__tests__/40-rate-limit-tester.test.ts +192 -0
- package/agents/__tests__/41-api-pagination-tester.test.ts +198 -0
- package/agents/__tests__/42-graphql-tester.test.ts +252 -0
- package/agents/__tests__/43-data-consistency-checker.test.ts +232 -0
- package/agents/__tests__/44-backup-recovery-tester.test.ts +222 -0
- package/agents/__tests__/45-data-privacy-scanner.test.ts +223 -0
- package/agents/__tests__/46-seo-auditor.test.ts +261 -0
- package/agents/__tests__/47-social-preview-tester.test.ts +245 -0
- package/agents/__tests__/48-lighthouse-auditor.test.ts +276 -0
- package/agents/__tests__/49-i18n-tester.test.ts +201 -0
- package/agents/__tests__/50-timezone-tester.test.ts +172 -0
- package/agents/__tests__/51-error-recovery-tester.test.ts +162 -0
- package/agents/__tests__/52-offline-mode-tester.test.ts +164 -0
- package/agents/__tests__/53-graceful-degradation-tester.test.ts +168 -0
- package/agents/__tests__/54-websocket-tester.test.ts +157 -0
- package/agents/__tests__/55-realtime-sync-tester.test.ts +181 -0
- package/agents/__tests__/56-file-upload-tester.test.ts +172 -0
- package/agents/__tests__/57-export-tester.test.ts +169 -0
- package/agents/__tests__/58-payment-flow-tester.test.ts +182 -0
- package/agents/__tests__/59-ssl-tls-auditor.test.ts +179 -0
- package/agents/__tests__/60-dns-cdn-tester.test.ts +176 -0
- package/agents/__tests__/61-docker-health-checker.test.ts +150 -0
- package/agents/__tests__/62-env-config-validator.test.ts +166 -0
- package/agents/__tests__/63-log-quality-auditor.test.ts +175 -0
- package/agents/__tests__/64-analytics-tracker-tester.test.ts +158 -0
- package/agents/__tests__/65-gdpr-compliance-tester.test.ts +174 -0
- package/agents/__tests__/66-soc2-control-validator.test.ts +183 -0
- package/agents/__tests__/67-wcag-aaa-tester.test.ts +190 -0
- package/agents/__tests__/68-dead-code-detector.test.ts +174 -0
- package/agents/__tests__/69-type-safety-auditor.test.ts +173 -0
- package/agents/__tests__/70-complexity-analyzer.test.ts +177 -0
- package/agents/__tests__/registry.test.ts +13 -13
- package/agents/registry.ts +146 -5
- package/core/__tests__/integration.test.ts +4 -4
- package/core/__tests__/orchestrator.test.ts +17 -16
- package/core/license.ts +208 -211
- package/core/orchestrator.ts +6 -4
- package/dist/agents/24-signup-onboarding-tester.d.ts +35 -0
- package/dist/agents/24-signup-onboarding-tester.d.ts.map +1 -0
- package/dist/agents/24-signup-onboarding-tester.js +357 -0
- package/dist/agents/24-signup-onboarding-tester.js.map +1 -0
- package/dist/agents/25-crud-flow-tester.d.ts +11 -0
- package/dist/agents/25-crud-flow-tester.d.ts.map +1 -0
- package/dist/agents/25-crud-flow-tester.js +253 -0
- package/dist/agents/25-crud-flow-tester.js.map +1 -0
- package/dist/agents/26-form-validator.d.ts +12 -0
- package/dist/agents/26-form-validator.d.ts.map +1 -0
- package/dist/agents/26-form-validator.js +257 -0
- package/dist/agents/26-form-validator.js.map +1 -0
- package/dist/agents/27-search-filter-tester.d.ts +20 -0
- package/dist/agents/27-search-filter-tester.d.ts.map +1 -0
- package/dist/agents/27-search-filter-tester.js +267 -0
- package/dist/agents/27-search-filter-tester.js.map +1 -0
- package/dist/agents/28-navigation-routing-tester.d.ts +32 -0
- package/dist/agents/28-navigation-routing-tester.d.ts.map +1 -0
- package/dist/agents/28-navigation-routing-tester.js +363 -0
- package/dist/agents/28-navigation-routing-tester.js.map +1 -0
- package/dist/agents/29-responsive-interaction-tester.d.ts +26 -0
- package/dist/agents/29-responsive-interaction-tester.d.ts.map +1 -0
- package/dist/agents/29-responsive-interaction-tester.js +272 -0
- package/dist/agents/29-responsive-interaction-tester.js.map +1 -0
- package/dist/agents/30-multi-user-scenario-tester.d.ts +24 -0
- package/dist/agents/30-multi-user-scenario-tester.d.ts.map +1 -0
- package/dist/agents/30-multi-user-scenario-tester.js +254 -0
- package/dist/agents/30-multi-user-scenario-tester.js.map +1 -0
- package/dist/agents/31-load-tester.d.ts +12 -0
- package/dist/agents/31-load-tester.d.ts.map +1 -0
- package/dist/agents/31-load-tester.js +110 -0
- package/dist/agents/31-load-tester.js.map +1 -0
- package/dist/agents/32-memory-leak-detector.d.ts +12 -0
- package/dist/agents/32-memory-leak-detector.d.ts.map +1 -0
- package/dist/agents/32-memory-leak-detector.js +167 -0
- package/dist/agents/32-memory-leak-detector.js.map +1 -0
- package/dist/agents/33-bundle-analyzer.d.ts +10 -0
- package/dist/agents/33-bundle-analyzer.d.ts.map +1 -0
- package/dist/agents/33-bundle-analyzer.js +111 -0
- package/dist/agents/33-bundle-analyzer.js.map +1 -0
- package/dist/agents/34-xss-scanner.d.ts +11 -0
- package/dist/agents/34-xss-scanner.d.ts.map +1 -0
- package/dist/agents/34-xss-scanner.js +164 -0
- package/dist/agents/34-xss-scanner.js.map +1 -0
- package/dist/agents/35-csrf-tester.d.ts +11 -0
- package/dist/agents/35-csrf-tester.d.ts.map +1 -0
- package/dist/agents/35-csrf-tester.js +70 -0
- package/dist/agents/35-csrf-tester.js.map +1 -0
- package/dist/agents/36-auth-fuzzer.d.ts +13 -0
- package/dist/agents/36-auth-fuzzer.d.ts.map +1 -0
- package/dist/agents/36-auth-fuzzer.js +163 -0
- package/dist/agents/36-auth-fuzzer.js.map +1 -0
- package/dist/agents/37-dependency-scanner.d.ts +11 -0
- package/dist/agents/37-dependency-scanner.d.ts.map +1 -0
- package/dist/agents/37-dependency-scanner.js +139 -0
- package/dist/agents/37-dependency-scanner.js.map +1 -0
- package/dist/agents/38-secrets-scanner.d.ts +11 -0
- package/dist/agents/38-secrets-scanner.d.ts.map +1 -0
- package/dist/agents/38-secrets-scanner.js +116 -0
- package/dist/agents/38-secrets-scanner.js.map +1 -0
- package/dist/agents/39-api-contract-tester.d.ts +12 -0
- package/dist/agents/39-api-contract-tester.d.ts.map +1 -0
- package/dist/agents/39-api-contract-tester.js +142 -0
- package/dist/agents/39-api-contract-tester.js.map +1 -0
- package/dist/agents/40-rate-limit-tester.d.ts +12 -0
- package/dist/agents/40-rate-limit-tester.d.ts.map +1 -0
- package/dist/agents/40-rate-limit-tester.js +79 -0
- package/dist/agents/40-rate-limit-tester.js.map +1 -0
- package/dist/agents/41-api-pagination-tester.d.ts +12 -0
- package/dist/agents/41-api-pagination-tester.d.ts.map +1 -0
- package/dist/agents/41-api-pagination-tester.js +79 -0
- package/dist/agents/41-api-pagination-tester.js.map +1 -0
- package/dist/agents/42-graphql-tester.d.ts +13 -0
- package/dist/agents/42-graphql-tester.d.ts.map +1 -0
- package/dist/agents/42-graphql-tester.js +187 -0
- package/dist/agents/42-graphql-tester.js.map +1 -0
- package/dist/agents/43-data-consistency-checker.d.ts +11 -0
- package/dist/agents/43-data-consistency-checker.d.ts.map +1 -0
- package/dist/agents/43-data-consistency-checker.js +176 -0
- package/dist/agents/43-data-consistency-checker.js.map +1 -0
- package/dist/agents/44-backup-recovery-tester.d.ts +11 -0
- package/dist/agents/44-backup-recovery-tester.d.ts.map +1 -0
- package/dist/agents/44-backup-recovery-tester.js +128 -0
- package/dist/agents/44-backup-recovery-tester.js.map +1 -0
- package/dist/agents/45-data-privacy-scanner.d.ts +11 -0
- package/dist/agents/45-data-privacy-scanner.d.ts.map +1 -0
- package/dist/agents/45-data-privacy-scanner.js +100 -0
- package/dist/agents/45-data-privacy-scanner.js.map +1 -0
- package/dist/agents/46-seo-auditor.d.ts +12 -0
- package/dist/agents/46-seo-auditor.d.ts.map +1 -0
- package/dist/agents/46-seo-auditor.js +275 -0
- package/dist/agents/46-seo-auditor.js.map +1 -0
- package/dist/agents/47-social-preview-tester.d.ts +11 -0
- package/dist/agents/47-social-preview-tester.d.ts.map +1 -0
- package/dist/agents/47-social-preview-tester.js +219 -0
- package/dist/agents/47-social-preview-tester.js.map +1 -0
- package/dist/agents/48-lighthouse-auditor.d.ts +11 -0
- package/dist/agents/48-lighthouse-auditor.d.ts.map +1 -0
- package/dist/agents/48-lighthouse-auditor.js +192 -0
- package/dist/agents/48-lighthouse-auditor.js.map +1 -0
- package/dist/agents/49-i18n-tester.d.ts +13 -0
- package/dist/agents/49-i18n-tester.d.ts.map +1 -0
- package/dist/agents/49-i18n-tester.js +172 -0
- package/dist/agents/49-i18n-tester.js.map +1 -0
- package/dist/agents/50-timezone-tester.d.ts +11 -0
- package/dist/agents/50-timezone-tester.d.ts.map +1 -0
- package/dist/agents/50-timezone-tester.js +152 -0
- package/dist/agents/50-timezone-tester.js.map +1 -0
- package/dist/agents/51-error-recovery-tester.d.ts +11 -0
- package/dist/agents/51-error-recovery-tester.d.ts.map +1 -0
- package/dist/agents/51-error-recovery-tester.js +134 -0
- package/dist/agents/51-error-recovery-tester.js.map +1 -0
- package/dist/agents/52-offline-mode-tester.d.ts +12 -0
- package/dist/agents/52-offline-mode-tester.d.ts.map +1 -0
- package/dist/agents/52-offline-mode-tester.js +161 -0
- package/dist/agents/52-offline-mode-tester.js.map +1 -0
- package/dist/agents/53-graceful-degradation-tester.d.ts +12 -0
- package/dist/agents/53-graceful-degradation-tester.d.ts.map +1 -0
- package/dist/agents/53-graceful-degradation-tester.js +130 -0
- package/dist/agents/53-graceful-degradation-tester.js.map +1 -0
- package/dist/agents/54-websocket-tester.d.ts +10 -0
- package/dist/agents/54-websocket-tester.d.ts.map +1 -0
- package/dist/agents/54-websocket-tester.js +132 -0
- package/dist/agents/54-websocket-tester.js.map +1 -0
- package/dist/agents/55-realtime-sync-tester.d.ts +11 -0
- package/dist/agents/55-realtime-sync-tester.d.ts.map +1 -0
- package/dist/agents/55-realtime-sync-tester.js +175 -0
- package/dist/agents/55-realtime-sync-tester.js.map +1 -0
- package/dist/agents/56-file-upload-tester.d.ts +12 -0
- package/dist/agents/56-file-upload-tester.d.ts.map +1 -0
- package/dist/agents/56-file-upload-tester.js +166 -0
- package/dist/agents/56-file-upload-tester.js.map +1 -0
- package/dist/agents/57-export-tester.d.ts +11 -0
- package/dist/agents/57-export-tester.d.ts.map +1 -0
- package/dist/agents/57-export-tester.js +155 -0
- package/dist/agents/57-export-tester.js.map +1 -0
- package/dist/agents/58-payment-flow-tester.d.ts +11 -0
- package/dist/agents/58-payment-flow-tester.d.ts.map +1 -0
- package/dist/agents/58-payment-flow-tester.js +159 -0
- package/dist/agents/58-payment-flow-tester.js.map +1 -0
- package/dist/agents/59-ssl-tls-auditor.d.ts +10 -0
- package/dist/agents/59-ssl-tls-auditor.d.ts.map +1 -0
- package/dist/agents/59-ssl-tls-auditor.js +132 -0
- package/dist/agents/59-ssl-tls-auditor.js.map +1 -0
- package/dist/agents/60-dns-cdn-tester.d.ts +10 -0
- package/dist/agents/60-dns-cdn-tester.d.ts.map +1 -0
- package/dist/agents/60-dns-cdn-tester.js +105 -0
- package/dist/agents/60-dns-cdn-tester.js.map +1 -0
- package/dist/agents/61-docker-health-checker.d.ts +10 -0
- package/dist/agents/61-docker-health-checker.d.ts.map +1 -0
- package/dist/agents/61-docker-health-checker.js +95 -0
- package/dist/agents/61-docker-health-checker.js.map +1 -0
- package/dist/agents/62-env-config-validator.d.ts +10 -0
- package/dist/agents/62-env-config-validator.d.ts.map +1 -0
- package/dist/agents/62-env-config-validator.js +132 -0
- package/dist/agents/62-env-config-validator.js.map +1 -0
- package/dist/agents/63-log-quality-auditor.d.ts +9 -0
- package/dist/agents/63-log-quality-auditor.d.ts.map +1 -0
- package/dist/agents/63-log-quality-auditor.js +121 -0
- package/dist/agents/63-log-quality-auditor.js.map +1 -0
- package/dist/agents/64-analytics-tracker-tester.d.ts +10 -0
- package/dist/agents/64-analytics-tracker-tester.d.ts.map +1 -0
- package/dist/agents/64-analytics-tracker-tester.js +146 -0
- package/dist/agents/64-analytics-tracker-tester.js.map +1 -0
- package/dist/agents/65-gdpr-compliance-tester.d.ts +13 -0
- package/dist/agents/65-gdpr-compliance-tester.d.ts.map +1 -0
- package/dist/agents/65-gdpr-compliance-tester.js +186 -0
- package/dist/agents/65-gdpr-compliance-tester.js.map +1 -0
- package/dist/agents/66-soc2-control-validator.d.ts +14 -0
- package/dist/agents/66-soc2-control-validator.d.ts.map +1 -0
- package/dist/agents/66-soc2-control-validator.js +178 -0
- package/dist/agents/66-soc2-control-validator.js.map +1 -0
- package/dist/agents/67-wcag-aaa-tester.d.ts +13 -0
- package/dist/agents/67-wcag-aaa-tester.d.ts.map +1 -0
- package/dist/agents/67-wcag-aaa-tester.js +207 -0
- package/dist/agents/67-wcag-aaa-tester.js.map +1 -0
- package/dist/agents/68-dead-code-detector.d.ts +9 -0
- package/dist/agents/68-dead-code-detector.d.ts.map +1 -0
- package/dist/agents/68-dead-code-detector.js +116 -0
- package/dist/agents/68-dead-code-detector.js.map +1 -0
- package/dist/agents/69-type-safety-auditor.d.ts +9 -0
- package/dist/agents/69-type-safety-auditor.d.ts.map +1 -0
- package/dist/agents/69-type-safety-auditor.js +148 -0
- package/dist/agents/69-type-safety-auditor.js.map +1 -0
- package/dist/agents/70-complexity-analyzer.d.ts +10 -0
- package/dist/agents/70-complexity-analyzer.d.ts.map +1 -0
- package/dist/agents/70-complexity-analyzer.js +154 -0
- package/dist/agents/70-complexity-analyzer.js.map +1 -0
- package/dist/agents/registry.d.ts +3 -3
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +146 -5
- package/dist/agents/registry.js.map +1 -1
- package/dist/core/license.js +2 -5
- package/dist/core/license.js.map +1 -1
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +6 -4
- package/dist/core/orchestrator.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Browser, Page } from 'playwright';
|
|
2
|
+
import type { Finding } from '../core/types';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
5
|
+
import { login } from '../helpers/navigation';
|
|
6
|
+
|
|
7
|
+
const NAV_TIMEOUT_MS = 10_000;
|
|
8
|
+
|
|
9
|
+
export class LighthouseAuditorAgent extends BaseAgent {
|
|
10
|
+
readonly agentId = 48;
|
|
11
|
+
readonly agentName = 'Lighthouse Auditor';
|
|
12
|
+
private baseUrl = '';
|
|
13
|
+
|
|
14
|
+
protected async preFlight(): Promise<void> {
|
|
15
|
+
if (!this.config.modules || this.config.modules.length === 0) {
|
|
16
|
+
throw new Error('LighthouseAuditorAgent requires at least one module in config.modules');
|
|
17
|
+
}
|
|
18
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
19
|
+
this.baseUrl = env.baseUrl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected async execute(): Promise<Finding[]> {
|
|
23
|
+
const findings: Finding[] = [];
|
|
24
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
25
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${this.baseUrl}${rawLoginUrl}`;
|
|
26
|
+
const credentials =
|
|
27
|
+
this.config.auth.credentials?.['admin'] ??
|
|
28
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
29
|
+
|
|
30
|
+
const { chromium } = await import('playwright');
|
|
31
|
+
let browser: Browser | null = null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
browser = await chromium.launch({ headless: true });
|
|
35
|
+
const page = await browser.newPage();
|
|
36
|
+
|
|
37
|
+
if (credentials) {
|
|
38
|
+
try {
|
|
39
|
+
await login(page, credentials, loginUrl);
|
|
40
|
+
} catch (loginError) {
|
|
41
|
+
findings.push({
|
|
42
|
+
id: `${this.agentId}-login-failed`,
|
|
43
|
+
type: 'infra-issue',
|
|
44
|
+
severity: 'medium',
|
|
45
|
+
agentId: this.agentId,
|
|
46
|
+
module: 'lighthouse-auditor',
|
|
47
|
+
description: `Login failed — continuing anonymously: ${loginError instanceof Error ? loginError.message.split('\n')[0] : String(loginError)}`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const mod of this.config.modules) {
|
|
53
|
+
const moduleFindings = await this.auditModule(page, mod.id, mod.route);
|
|
54
|
+
findings.push(...moduleFindings);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await page.close().catch(() => undefined);
|
|
58
|
+
} catch (playwrightError) {
|
|
59
|
+
findings.push({
|
|
60
|
+
id: `${this.agentId}-playwright-failure`,
|
|
61
|
+
type: 'infra-issue',
|
|
62
|
+
severity: 'medium',
|
|
63
|
+
agentId: this.agentId,
|
|
64
|
+
module: 'lighthouse-auditor',
|
|
65
|
+
description: `Playwright execution failed: ${playwrightError instanceof Error ? playwrightError.message.split('\n')[0] : String(playwrightError)}`,
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async auditModule(
|
|
75
|
+
page: Page,
|
|
76
|
+
moduleId: string,
|
|
77
|
+
moduleRoute: string,
|
|
78
|
+
): Promise<Finding[]> {
|
|
79
|
+
const findings: Finding[] = [];
|
|
80
|
+
const fullUrl = moduleRoute.startsWith('http')
|
|
81
|
+
? moduleRoute
|
|
82
|
+
: `${this.baseUrl}${moduleRoute}`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await Promise.race([
|
|
86
|
+
page.goto(fullUrl, { waitUntil: 'domcontentloaded' }),
|
|
87
|
+
new Promise((_, reject) =>
|
|
88
|
+
setTimeout(() => reject(new Error('Navigation timeout')), NAV_TIMEOUT_MS),
|
|
89
|
+
),
|
|
90
|
+
]);
|
|
91
|
+
} catch {
|
|
92
|
+
findings.push({
|
|
93
|
+
id: `${this.agentId}-nav-failed-${moduleId}`,
|
|
94
|
+
type: 'test-bug',
|
|
95
|
+
severity: 'medium',
|
|
96
|
+
agentId: this.agentId,
|
|
97
|
+
module: moduleId,
|
|
98
|
+
description: `Failed to navigate to module ${moduleId} at ${moduleRoute}`,
|
|
99
|
+
});
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Measure performance timing
|
|
104
|
+
try {
|
|
105
|
+
const metrics = await page.evaluate(() => {
|
|
106
|
+
const timing = performance.timing;
|
|
107
|
+
const navigationStart = timing.navigationStart;
|
|
108
|
+
const entries = performance.getEntriesByType('paint') as PerformancePaintTiming[];
|
|
109
|
+
const fcp = entries.find(e => e.name === 'first-contentful-paint');
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
domContentLoaded: timing.domContentLoadedEventEnd - navigationStart,
|
|
113
|
+
loadComplete: timing.loadEventEnd - navigationStart,
|
|
114
|
+
fcp: fcp?.startTime ?? null,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const fcpThreshold = this.config.thresholds?.lighthouse?.performance
|
|
119
|
+
? (100 - this.config.thresholds.lighthouse.performance) * 50 + 1000
|
|
120
|
+
: 2500;
|
|
121
|
+
|
|
122
|
+
if (metrics.fcp !== null && metrics.fcp > fcpThreshold) {
|
|
123
|
+
findings.push({
|
|
124
|
+
id: `${this.agentId}-slow-fcp-${moduleId}`,
|
|
125
|
+
type: 'test-bug',
|
|
126
|
+
severity: 'medium',
|
|
127
|
+
agentId: this.agentId,
|
|
128
|
+
module: moduleId,
|
|
129
|
+
description: `Module ${moduleId} First Contentful Paint is ${metrics.fcp.toFixed(0)}ms (threshold: ${fcpThreshold}ms)`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (metrics.domContentLoaded > 3000) {
|
|
134
|
+
findings.push({
|
|
135
|
+
id: `${this.agentId}-slow-dcl-${moduleId}`,
|
|
136
|
+
type: 'test-bug',
|
|
137
|
+
severity: 'medium',
|
|
138
|
+
agentId: this.agentId,
|
|
139
|
+
module: moduleId,
|
|
140
|
+
description: `Module ${moduleId} DOMContentLoaded took ${metrics.domContentLoaded}ms`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.addEvidence({
|
|
145
|
+
type: 'log',
|
|
146
|
+
path: `perf-${moduleId}`,
|
|
147
|
+
description: `Module ${moduleId}: FCP=${metrics.fcp?.toFixed(0) ?? 'N/A'}ms, DCL=${metrics.domContentLoaded}ms, Load=${metrics.loadComplete}ms`,
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
// Performance metrics unavailable
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check <meta name="viewport">
|
|
154
|
+
try {
|
|
155
|
+
const hasViewport = await page.evaluate(() => {
|
|
156
|
+
return document.querySelector('meta[name="viewport"]') !== null;
|
|
157
|
+
});
|
|
158
|
+
if (!hasViewport) {
|
|
159
|
+
findings.push({
|
|
160
|
+
id: `${this.agentId}-no-viewport-${moduleId}`,
|
|
161
|
+
type: 'test-bug',
|
|
162
|
+
severity: 'medium',
|
|
163
|
+
agentId: this.agentId,
|
|
164
|
+
module: moduleId,
|
|
165
|
+
description: `Module ${moduleId} is missing <meta name="viewport">`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// Could not evaluate viewport
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check <html lang>
|
|
173
|
+
try {
|
|
174
|
+
const hasLang = await page.evaluate(() => {
|
|
175
|
+
return document.documentElement.hasAttribute('lang');
|
|
176
|
+
});
|
|
177
|
+
if (!hasLang) {
|
|
178
|
+
findings.push({
|
|
179
|
+
id: `${this.agentId}-no-lang-${moduleId}`,
|
|
180
|
+
type: 'test-bug',
|
|
181
|
+
severity: 'medium',
|
|
182
|
+
agentId: this.agentId,
|
|
183
|
+
module: moduleId,
|
|
184
|
+
description: `Module ${moduleId} is missing lang attribute on <html>`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Could not evaluate lang
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check <meta charset>
|
|
192
|
+
try {
|
|
193
|
+
const hasCharset = await page.evaluate(() => {
|
|
194
|
+
return document.querySelector('meta[charset]') !== null ||
|
|
195
|
+
document.querySelector('meta[http-equiv="Content-Type"]') !== null;
|
|
196
|
+
});
|
|
197
|
+
if (!hasCharset) {
|
|
198
|
+
findings.push({
|
|
199
|
+
id: `${this.agentId}-no-charset-${moduleId}`,
|
|
200
|
+
type: 'test-bug',
|
|
201
|
+
severity: 'low',
|
|
202
|
+
agentId: this.agentId,
|
|
203
|
+
module: moduleId,
|
|
204
|
+
description: `Module ${moduleId} is missing <meta charset>`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Could not evaluate charset
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return findings;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Browser, Page } from 'playwright';
|
|
2
|
+
import type { Finding } from '../core/types';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
5
|
+
import { login } from '../helpers/navigation';
|
|
6
|
+
|
|
7
|
+
/** Pattern that detects raw i18n keys like home.title, nav.dashboard.items */
|
|
8
|
+
const I18N_KEY_PATTERN = /\b[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*/;
|
|
9
|
+
|
|
10
|
+
/** Selectors for language switcher UI elements. */
|
|
11
|
+
const LANG_SWITCHER_SELECTORS = [
|
|
12
|
+
'[data-testid*="lang"]',
|
|
13
|
+
'[data-testid*="locale"]',
|
|
14
|
+
'[data-testid*="language"]',
|
|
15
|
+
'select[name*="lang"]',
|
|
16
|
+
'select[name*="locale"]',
|
|
17
|
+
'button:has-text("English")',
|
|
18
|
+
'button:has-text("EN")',
|
|
19
|
+
'[aria-label*="language"]',
|
|
20
|
+
'[aria-label*="locale"]',
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
/** RTL locales to check. */
|
|
24
|
+
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'] as const;
|
|
25
|
+
|
|
26
|
+
export class I18nTesterAgent extends BaseAgent {
|
|
27
|
+
readonly agentId = 49;
|
|
28
|
+
readonly agentName = 'i18n Tester';
|
|
29
|
+
private baseUrl = '';
|
|
30
|
+
|
|
31
|
+
protected async preFlight(): Promise<void> {
|
|
32
|
+
if (!this.config.modules || this.config.modules.length === 0) {
|
|
33
|
+
throw new Error('I18nTesterAgent requires at least one module in config.modules');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
37
|
+
this.baseUrl = env.baseUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected async execute(): Promise<Finding[]> {
|
|
41
|
+
const findings: Finding[] = [];
|
|
42
|
+
|
|
43
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
44
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${this.baseUrl}${rawLoginUrl}`;
|
|
45
|
+
const credentials =
|
|
46
|
+
this.config.auth.credentials?.['admin'] ??
|
|
47
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
48
|
+
|
|
49
|
+
const { chromium } = await import('playwright');
|
|
50
|
+
|
|
51
|
+
let browser: Browser | null = null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
browser = await chromium.launch({ headless: true });
|
|
55
|
+
const page = await browser.newPage();
|
|
56
|
+
|
|
57
|
+
if (credentials) {
|
|
58
|
+
try {
|
|
59
|
+
await login(page, credentials, loginUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
findings.push({
|
|
62
|
+
id: `${this.agentId}-login-failed`,
|
|
63
|
+
type: 'infra-issue',
|
|
64
|
+
severity: 'medium',
|
|
65
|
+
agentId: this.agentId,
|
|
66
|
+
module: 'i18n-tester',
|
|
67
|
+
description: 'Login failed — continuing anonymously',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for language switcher
|
|
73
|
+
const langSwitcherFound = await this.findLanguageSwitcher(page);
|
|
74
|
+
|
|
75
|
+
if (!langSwitcherFound) {
|
|
76
|
+
findings.push({
|
|
77
|
+
id: `${this.agentId}-no-lang-switcher`,
|
|
78
|
+
type: 'test-bug',
|
|
79
|
+
severity: 'low',
|
|
80
|
+
agentId: this.agentId,
|
|
81
|
+
module: 'i18n-tester',
|
|
82
|
+
description: 'No language switcher UI element found',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check pages for untranslated strings
|
|
87
|
+
for (const mod of this.config.modules) {
|
|
88
|
+
const moduleFindings = await this.checkModule(page, mod.id, mod.route);
|
|
89
|
+
findings.push(...moduleFindings);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check RTL layout if language switcher found
|
|
93
|
+
if (langSwitcherFound) {
|
|
94
|
+
const rtlFindings = await this.checkRtlLayout(page);
|
|
95
|
+
findings.push(...rtlFindings);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await page.close().catch(() => undefined);
|
|
99
|
+
} catch (playwrightError) {
|
|
100
|
+
findings.push({
|
|
101
|
+
id: `${this.agentId}-playwright-failure`,
|
|
102
|
+
type: 'infra-issue',
|
|
103
|
+
severity: 'medium',
|
|
104
|
+
agentId: this.agentId,
|
|
105
|
+
module: 'i18n-tester',
|
|
106
|
+
description: `Playwright execution failed: ${playwrightError instanceof Error ? playwrightError.message.split('\n')[0] : String(playwrightError)}`,
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async findLanguageSwitcher(page: Page): Promise<boolean> {
|
|
116
|
+
const selector = LANG_SWITCHER_SELECTORS.join(', ');
|
|
117
|
+
try {
|
|
118
|
+
const count = await page.locator(selector).count();
|
|
119
|
+
return count > 0;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async checkModule(
|
|
126
|
+
page: Page,
|
|
127
|
+
moduleId: string,
|
|
128
|
+
moduleRoute: string,
|
|
129
|
+
): Promise<Finding[]> {
|
|
130
|
+
const findings: Finding[] = [];
|
|
131
|
+
const fullUrl = moduleRoute.startsWith('http')
|
|
132
|
+
? moduleRoute
|
|
133
|
+
: `${this.baseUrl}${moduleRoute}`;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: 10_000 });
|
|
137
|
+
} catch {
|
|
138
|
+
findings.push({
|
|
139
|
+
id: `${this.agentId}-nav-failed-${moduleId}`,
|
|
140
|
+
type: 'test-bug',
|
|
141
|
+
severity: 'medium',
|
|
142
|
+
agentId: this.agentId,
|
|
143
|
+
module: moduleId,
|
|
144
|
+
description: `Failed to navigate to module ${moduleId} at ${moduleRoute}`,
|
|
145
|
+
});
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for untranslated i18n keys
|
|
150
|
+
try {
|
|
151
|
+
const bodyText = await page.evaluate(() => document.body.innerText);
|
|
152
|
+
const lines = bodyText.split('\n');
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
if (I18N_KEY_PATTERN.test(line.trim()) && !line.includes(' ')) {
|
|
155
|
+
findings.push({
|
|
156
|
+
id: `${this.agentId}-untranslated-${moduleId}`,
|
|
157
|
+
type: 'test-bug',
|
|
158
|
+
severity: 'medium',
|
|
159
|
+
agentId: this.agentId,
|
|
160
|
+
module: moduleId,
|
|
161
|
+
description: `Possible untranslated i18n key found: "${line.trim().substring(0, 80)}"`,
|
|
162
|
+
});
|
|
163
|
+
break; // Report one per module
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// evaluate failed — non-critical
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return findings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async checkRtlLayout(page: Page): Promise<Finding[]> {
|
|
174
|
+
const findings: Finding[] = [];
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const dir = await page.evaluate(() => document.dir || document.documentElement.dir);
|
|
178
|
+
const lang = await page.evaluate(() => document.documentElement.lang);
|
|
179
|
+
|
|
180
|
+
const isRtlLocale = RTL_LOCALES.some(locale => lang?.startsWith(locale));
|
|
181
|
+
|
|
182
|
+
if (isRtlLocale && dir !== 'rtl') {
|
|
183
|
+
findings.push({
|
|
184
|
+
id: `${this.agentId}-rtl-missing`,
|
|
185
|
+
type: 'test-bug',
|
|
186
|
+
severity: 'medium',
|
|
187
|
+
agentId: this.agentId,
|
|
188
|
+
module: 'i18n-tester',
|
|
189
|
+
description: `RTL locale "${lang}" detected but document.dir is "${dir ?? 'not set'}" instead of "rtl"`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// evaluate failed — non-critical
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
2
|
+
import type { Finding } from '../core/types';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
5
|
+
import { login } from '../helpers/navigation';
|
|
6
|
+
|
|
7
|
+
/** Timezones to test across. */
|
|
8
|
+
const TEST_TIMEZONES = ['UTC', 'America/New_York', 'Australia/Sydney'] as const;
|
|
9
|
+
|
|
10
|
+
/** Selectors that commonly hold date values. */
|
|
11
|
+
const DATE_SELECTORS = 'time, [datetime], .date, [data-date], [data-testid*="date"]';
|
|
12
|
+
|
|
13
|
+
export class TimezoneTesterAgent extends BaseAgent {
|
|
14
|
+
readonly agentId = 50;
|
|
15
|
+
readonly agentName = 'Timezone Tester';
|
|
16
|
+
private baseUrl = '';
|
|
17
|
+
|
|
18
|
+
protected async preFlight(): Promise<void> {
|
|
19
|
+
if (!this.config.modules || this.config.modules.length === 0) {
|
|
20
|
+
throw new Error('TimezoneTesterAgent requires at least one module in config.modules');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
24
|
+
this.baseUrl = env.baseUrl;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected async execute(): Promise<Finding[]> {
|
|
28
|
+
const findings: Finding[] = [];
|
|
29
|
+
|
|
30
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
31
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${this.baseUrl}${rawLoginUrl}`;
|
|
32
|
+
const credentials =
|
|
33
|
+
this.config.auth.credentials?.['admin'] ??
|
|
34
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
35
|
+
|
|
36
|
+
const { chromium } = await import('playwright');
|
|
37
|
+
|
|
38
|
+
let browser: Browser | null = null;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
browser = await chromium.launch({ headless: true });
|
|
42
|
+
|
|
43
|
+
const firstModule = this.config.modules[0];
|
|
44
|
+
const fullUrl = firstModule.route.startsWith('http')
|
|
45
|
+
? firstModule.route
|
|
46
|
+
: `${this.baseUrl}${firstModule.route}`;
|
|
47
|
+
|
|
48
|
+
const datesByTimezone: Record<string, string[]> = {};
|
|
49
|
+
|
|
50
|
+
for (const tz of TEST_TIMEZONES) {
|
|
51
|
+
const context = await browser.newContext({ timezoneId: tz });
|
|
52
|
+
const page = await context.newPage();
|
|
53
|
+
|
|
54
|
+
if (credentials) {
|
|
55
|
+
try {
|
|
56
|
+
await login(page, credentials, loginUrl);
|
|
57
|
+
} catch {
|
|
58
|
+
// Continue without auth
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: 10_000 });
|
|
64
|
+
} catch {
|
|
65
|
+
findings.push({
|
|
66
|
+
id: `${this.agentId}-nav-failed-${tz}`,
|
|
67
|
+
type: 'test-bug',
|
|
68
|
+
severity: 'medium',
|
|
69
|
+
agentId: this.agentId,
|
|
70
|
+
module: firstModule.id,
|
|
71
|
+
description: `Failed to navigate to ${firstModule.id} with timezone ${tz}`,
|
|
72
|
+
});
|
|
73
|
+
await context.close().catch(() => undefined);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Read date elements
|
|
78
|
+
try {
|
|
79
|
+
const dates = await page.evaluate((selector: string) => {
|
|
80
|
+
const elements = document.querySelectorAll(selector);
|
|
81
|
+
return Array.from(elements).map(el => el.textContent?.trim() ?? '');
|
|
82
|
+
}, DATE_SELECTORS);
|
|
83
|
+
|
|
84
|
+
datesByTimezone[tz] = dates.filter(d => d.length > 0);
|
|
85
|
+
} catch {
|
|
86
|
+
datesByTimezone[tz] = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await page.close().catch(() => undefined);
|
|
90
|
+
await context.close().catch(() => undefined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check format consistency across timezones
|
|
94
|
+
const timezoneKeys = Object.keys(datesByTimezone);
|
|
95
|
+
if (timezoneKeys.length >= 2) {
|
|
96
|
+
const firstTzDates = datesByTimezone[timezoneKeys[0]];
|
|
97
|
+
const otherTzKeys = timezoneKeys.slice(1);
|
|
98
|
+
|
|
99
|
+
for (const otherTz of otherTzKeys) {
|
|
100
|
+
const otherDates = datesByTimezone[otherTz];
|
|
101
|
+
if (firstTzDates.length !== otherDates.length) {
|
|
102
|
+
findings.push({
|
|
103
|
+
id: `${this.agentId}-date-count-mismatch-${otherTz}`,
|
|
104
|
+
type: 'test-bug',
|
|
105
|
+
severity: 'low',
|
|
106
|
+
agentId: this.agentId,
|
|
107
|
+
module: firstModule.id,
|
|
108
|
+
description: `Date element count differs between ${timezoneKeys[0]} (${firstTzDates.length}) and ${otherTz} (${otherDates.length})`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for day-level differences (off-by-one)
|
|
113
|
+
const minLen = Math.min(firstTzDates.length, otherDates.length);
|
|
114
|
+
for (let i = 0; i < minLen; i++) {
|
|
115
|
+
const dayDiff = this.detectDayDifference(firstTzDates[i], otherDates[i]);
|
|
116
|
+
if (dayDiff) {
|
|
117
|
+
findings.push({
|
|
118
|
+
id: `${this.agentId}-off-by-one-${otherTz}-${i}`,
|
|
119
|
+
type: 'test-bug',
|
|
120
|
+
severity: 'medium',
|
|
121
|
+
agentId: this.agentId,
|
|
122
|
+
module: firstModule.id,
|
|
123
|
+
description: `Potential off-by-one date: "${firstTzDates[i]}" (${timezoneKeys[0]}) vs "${otherDates[i]}" (${otherTz})`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (timezoneKeys.every(tz => datesByTimezone[tz].length === 0)) {
|
|
130
|
+
findings.push({
|
|
131
|
+
id: `${this.agentId}-no-dates-found`,
|
|
132
|
+
type: 'test-bug',
|
|
133
|
+
severity: 'low',
|
|
134
|
+
agentId: this.agentId,
|
|
135
|
+
module: firstModule.id,
|
|
136
|
+
description: 'No date elements found on the page to test timezone behavior',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (playwrightError) {
|
|
141
|
+
findings.push({
|
|
142
|
+
id: `${this.agentId}-playwright-failure`,
|
|
143
|
+
type: 'infra-issue',
|
|
144
|
+
severity: 'medium',
|
|
145
|
+
agentId: this.agentId,
|
|
146
|
+
module: 'timezone-tester',
|
|
147
|
+
description: `Playwright execution failed: ${playwrightError instanceof Error ? playwrightError.message.split('\n')[0] : String(playwrightError)}`,
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private detectDayDifference(dateA: string, dateB: string): boolean {
|
|
157
|
+
if (dateA === dateB) return false;
|
|
158
|
+
|
|
159
|
+
// Extract day numbers from both strings
|
|
160
|
+
const dayPatternA = dateA.match(/\b(\d{1,2})\b/g);
|
|
161
|
+
const dayPatternB = dateB.match(/\b(\d{1,2})\b/g);
|
|
162
|
+
|
|
163
|
+
if (!dayPatternA || !dayPatternB) return false;
|
|
164
|
+
|
|
165
|
+
// If any day number differs by exactly 1, it could be an off-by-one
|
|
166
|
+
for (let i = 0; i < Math.min(dayPatternA.length, dayPatternB.length); i++) {
|
|
167
|
+
const diff = Math.abs(parseInt(dayPatternA[i], 10) - parseInt(dayPatternB[i], 10));
|
|
168
|
+
if (diff === 1) return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|