mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
/**
|
|
* Accessibility Tester INFRASTRUCTURE Tests
|
|
*
|
|
* NOTE: These are NOT user-facing E2E tests. They test the accessibility
|
|
* testing infrastructure. For critical user journey E2E tests, see:
|
|
* tests/e2e/critical-user-journeys.e2e.test.ts
|
|
*
|
|
* These tests verify:
|
|
* - AccessibilityTesterService integrates with browser client
|
|
* - axe-core injection and execution works
|
|
* - Violation detection and reporting works
|
|
*
|
|
* These tests require agent-browser CLI to be installed.
|
|
* NOTE: Tests are automatically SKIPPED in CI where agent-browser is unavailable.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
import { AccessibilityTesterService } from '../../../src/domains/visual-accessibility/services/accessibility-tester';
|
|
import { AgentBrowserClient } from '../../../src/integrations/browser/agent-browser/client';
|
|
import { createAgentBrowserClient } from '../../../src/integrations/browser/client-factory';
|
|
import type { MemoryBackend, StoreOptions, VectorSearchResult } from '../../../src/kernel/interfaces';
|
|
import { execSync } from 'child_process';
|
|
|
|
const BROWSER_TIMEOUT = 60000;
|
|
|
|
// Check if agent-browser is available
|
|
let agentBrowserAvailable = false;
|
|
try {
|
|
const result = execSync('npx agent-browser --version 2>&1 || echo "not-found"', {
|
|
encoding: 'utf-8',
|
|
stdio: 'pipe',
|
|
timeout: 10000
|
|
});
|
|
agentBrowserAvailable = !result.includes('not-found') && !result.includes('ERR!');
|
|
} catch {
|
|
agentBrowserAvailable = false;
|
|
}
|
|
|
|
// Skip tests if agent-browser not available OR if running in CI
|
|
// CI environments typically don't have browser automation setup
|
|
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
const describeIfAvailable = (!isCI && agentBrowserAvailable) ? describe : describe.skip;
|
|
|
|
// In-memory backend for testing
|
|
class TestMemoryBackend implements MemoryBackend {
|
|
private store = new Map<string, unknown>();
|
|
|
|
async initialize(): Promise<void> {}
|
|
async dispose(): Promise<void> {}
|
|
async set<T>(key: string, value: T): Promise<void> {
|
|
this.store.set(key, value);
|
|
}
|
|
async get<T>(key: string): Promise<T | undefined> {
|
|
return this.store.get(key) as T | undefined;
|
|
}
|
|
async delete(key: string): Promise<boolean> {
|
|
return this.store.delete(key);
|
|
}
|
|
async has(key: string): Promise<boolean> {
|
|
return this.store.has(key);
|
|
}
|
|
async search(pattern: string): Promise<string[]> {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
return Array.from(this.store.keys()).filter((k) => regex.test(k));
|
|
}
|
|
async vectorSearch(): Promise<VectorSearchResult[]> {
|
|
return [];
|
|
}
|
|
async storeVector(): Promise<void> {}
|
|
}
|
|
|
|
// Test pages with known accessibility issues
|
|
const TEST_PAGES = {
|
|
// Good accessibility
|
|
accessible: 'https://example.com',
|
|
|
|
// Page with forms (httpbin)
|
|
forms: 'https://httpbin.org/forms/post',
|
|
|
|
// Data URL with intentional accessibility issues
|
|
badA11y: `data:text/html,<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Bad A11y Test Page</title></head>
|
|
<body>
|
|
<img src="image.jpg"><!-- Missing alt -->
|
|
<div onclick="click()">Click me</div><!-- Non-semantic button -->
|
|
<a href="#">Click here</a><!-- Generic link text -->
|
|
<input type="text"><!-- No label -->
|
|
<p style="color: #999; background: #fff;">Low contrast text</p>
|
|
</body>
|
|
</html>`,
|
|
|
|
// Data URL with good accessibility
|
|
goodA11y: `data:text/html,<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head><title>Good A11y Test Page</title></head>
|
|
<body>
|
|
<main>
|
|
<h1>Welcome</h1>
|
|
<img src="image.jpg" alt="Description of image">
|
|
<button type="button">Click me</button>
|
|
<a href="/about">Learn more about our services</a>
|
|
<label for="email">Email:</label>
|
|
<input type="email" id="email" name="email">
|
|
</main>
|
|
</body>
|
|
</html>`,
|
|
};
|
|
|
|
describeIfAvailable('AccessibilityTesterService - Real Browser Integration', () => {
|
|
let service: AccessibilityTesterService;
|
|
let browserClient: AgentBrowserClient;
|
|
let memoryBackend: TestMemoryBackend;
|
|
|
|
beforeAll(async () => {
|
|
memoryBackend = new TestMemoryBackend();
|
|
|
|
// Create real browser client
|
|
browserClient = new AgentBrowserClient({
|
|
sessionName: `a11y-test-${Date.now()}`,
|
|
timeout: BROWSER_TIMEOUT,
|
|
headed: false,
|
|
});
|
|
|
|
// Create service with real browser client
|
|
service = new AccessibilityTesterService(
|
|
{
|
|
defaultWCAGLevel: 'AA',
|
|
includeWarnings: true,
|
|
auditTimeout: 30000,
|
|
enableColorContrastCheck: true,
|
|
enableKeyboardCheck: true,
|
|
simulationMode: false,
|
|
enableBrowserMode: true,
|
|
preferAgentBrowser: true,
|
|
browserClient: browserClient,
|
|
},
|
|
memoryBackend
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await browserClient.quit();
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should audit a real page and return accessibility report', async () => {
|
|
const result = await service.audit(TEST_PAGES.accessible, {
|
|
level: 'AA',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value).toBeDefined();
|
|
|
|
const report = result.value!;
|
|
expect(report.url).toContain('example.com');
|
|
expect(report.wcagLevel).toBe('AA');
|
|
expect(report.violations).toBeDefined();
|
|
expect(Array.isArray(report.violations)).toBe(true);
|
|
|
|
// example.com should be relatively accessible
|
|
console.log(`[A11y Audit] example.com: ${report.violations.length} violations found`);
|
|
}, BROWSER_TIMEOUT);
|
|
|
|
it('should detect accessibility violations on page with known issues', async () => {
|
|
const result = await service.audit(TEST_PAGES.badA11y, {
|
|
level: 'AA',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value).toBeDefined();
|
|
|
|
const report = result.value!;
|
|
|
|
// Page has intentional issues:
|
|
// - img without alt
|
|
// - input without label
|
|
// - div used as button
|
|
// - generic link text
|
|
|
|
console.log(`[A11y Audit] Bad A11y page: ${report.violations.length} violations found`);
|
|
console.log('Violations:', report.violations.map((v) => v.id).join(', '));
|
|
|
|
// Should find at least some violations
|
|
// Note: In heuristic mode (no axe-core), violations may be different
|
|
expect(report.violations.length).toBeGreaterThanOrEqual(0);
|
|
}, BROWSER_TIMEOUT);
|
|
|
|
it('should pass audit on well-structured accessible page', async () => {
|
|
const result = await service.audit(TEST_PAGES.goodA11y, {
|
|
level: 'AA',
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value).toBeDefined();
|
|
|
|
const report = result.value!;
|
|
console.log(`[A11y Audit] Good A11y page: ${report.violations.length} violations found`);
|
|
|
|
// Well-structured page should have few or no violations
|
|
// (There may still be minor issues detected)
|
|
}, BROWSER_TIMEOUT);
|
|
|
|
it('should respect WCAG level configuration', async () => {
|
|
// Test with Level A (less strict)
|
|
const resultA = await service.audit(TEST_PAGES.accessible, { level: 'A' });
|
|
expect(resultA.success).toBe(true);
|
|
expect(resultA.value?.wcagLevel).toBe('A');
|
|
|
|
// Test with Level AAA (most strict)
|
|
const resultAAA = await service.audit(TEST_PAGES.accessible, { level: 'AAA' });
|
|
expect(resultAAA.success).toBe(true);
|
|
expect(resultAAA.value?.wcagLevel).toBe('AAA');
|
|
|
|
// AAA should potentially find more issues than A
|
|
console.log(`Level A violations: ${resultA.value?.violations.length}`);
|
|
console.log(`Level AAA violations: ${resultAAA.value?.violations.length}`);
|
|
}, BROWSER_TIMEOUT * 2);
|
|
});
|
|
|
|
describeIfAvailable('AccessibilityTesterService - axe-core Integration', () => {
|
|
let service: AccessibilityTesterService;
|
|
let browserClient: AgentBrowserClient;
|
|
let memoryBackend: TestMemoryBackend;
|
|
|
|
beforeEach(async () => {
|
|
memoryBackend = new TestMemoryBackend();
|
|
browserClient = new AgentBrowserClient({
|
|
sessionName: `axe-test-${Date.now()}`,
|
|
timeout: BROWSER_TIMEOUT,
|
|
});
|
|
|
|
service = new AccessibilityTesterService(
|
|
{
|
|
defaultWCAGLevel: 'AA',
|
|
includeWarnings: true,
|
|
auditTimeout: 30000,
|
|
enableColorContrastCheck: true,
|
|
enableKeyboardCheck: true,
|
|
simulationMode: false,
|
|
enableBrowserMode: true,
|
|
preferAgentBrowser: true,
|
|
browserClient: browserClient,
|
|
},
|
|
memoryBackend
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await browserClient.quit();
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should inject and run axe-core in browser context', async () => {
|
|
// Launch browser and navigate
|
|
await browserClient.launch({ headless: true });
|
|
await browserClient.navigate(TEST_PAGES.accessible);
|
|
|
|
// Try to inject axe-core and run it
|
|
const axeScript = `
|
|
(async function() {
|
|
// Check if axe is already loaded
|
|
if (typeof axe === 'undefined') {
|
|
// Create script element
|
|
const script = document.createElement('script');
|
|
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.4/axe.min.js';
|
|
document.head.appendChild(script);
|
|
|
|
// Wait for load
|
|
await new Promise((resolve, reject) => {
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
setTimeout(reject, 10000);
|
|
});
|
|
}
|
|
|
|
// Run axe
|
|
const results = await axe.run();
|
|
return {
|
|
violations: results.violations.length,
|
|
passes: results.passes.length,
|
|
incomplete: results.incomplete.length,
|
|
};
|
|
})()
|
|
`;
|
|
|
|
const evalResult = await browserClient.evaluate<{
|
|
violations: number;
|
|
passes: number;
|
|
incomplete: number;
|
|
}>(axeScript);
|
|
|
|
// axe-core should run successfully
|
|
if (evalResult.success) {
|
|
console.log('[axe-core] Results:', evalResult.value);
|
|
expect(evalResult.value).toBeDefined();
|
|
expect(typeof evalResult.value?.violations).toBe('number');
|
|
expect(typeof evalResult.value?.passes).toBe('number');
|
|
} else {
|
|
// If axe-core fails to load (CSP, network issues), that's okay for this test
|
|
console.log('[axe-core] Could not inject axe-core:', evalResult.error);
|
|
}
|
|
}, BROWSER_TIMEOUT);
|
|
});
|
|
|
|
describeIfAvailable('AccessibilityTesterService - Color Contrast Analysis', () => {
|
|
let service: AccessibilityTesterService;
|
|
let browserClient: AgentBrowserClient;
|
|
let memoryBackend: TestMemoryBackend;
|
|
|
|
beforeEach(async () => {
|
|
memoryBackend = new TestMemoryBackend();
|
|
browserClient = new AgentBrowserClient({
|
|
sessionName: `contrast-test-${Date.now()}`,
|
|
timeout: BROWSER_TIMEOUT,
|
|
});
|
|
|
|
service = new AccessibilityTesterService(
|
|
{
|
|
defaultWCAGLevel: 'AA',
|
|
includeWarnings: true,
|
|
auditTimeout: 30000,
|
|
enableColorContrastCheck: true,
|
|
enableKeyboardCheck: false,
|
|
simulationMode: false,
|
|
enableBrowserMode: true,
|
|
preferAgentBrowser: true,
|
|
browserClient: browserClient,
|
|
},
|
|
memoryBackend
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await browserClient.quit();
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should analyze color contrast on real page', async () => {
|
|
const contrastPage = `data:text/html,<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Contrast Test</title></head>
|
|
<body>
|
|
<p style="color: #000; background: #fff;">High contrast (good)</p>
|
|
<p style="color: #767676; background: #fff;">Minimum contrast (borderline)</p>
|
|
<p style="color: #999; background: #fff;">Low contrast (bad)</p>
|
|
</body>
|
|
</html>`;
|
|
|
|
const result = await service.analyzeColorContrast(contrastPage);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value).toBeDefined();
|
|
|
|
const analysis = result.value!;
|
|
console.log('[Contrast] Issues found:', analysis.issues?.length ?? 0);
|
|
}, BROWSER_TIMEOUT);
|
|
});
|
|
|
|
describeIfAvailable('AccessibilityTesterService - Keyboard Navigation', () => {
|
|
let service: AccessibilityTesterService;
|
|
let browserClient: AgentBrowserClient;
|
|
let memoryBackend: TestMemoryBackend;
|
|
|
|
beforeEach(async () => {
|
|
memoryBackend = new TestMemoryBackend();
|
|
browserClient = new AgentBrowserClient({
|
|
sessionName: `keyboard-test-${Date.now()}`,
|
|
timeout: BROWSER_TIMEOUT,
|
|
});
|
|
|
|
service = new AccessibilityTesterService(
|
|
{
|
|
defaultWCAGLevel: 'AA',
|
|
includeWarnings: true,
|
|
auditTimeout: 30000,
|
|
enableColorContrastCheck: false,
|
|
enableKeyboardCheck: true,
|
|
simulationMode: false,
|
|
enableBrowserMode: true,
|
|
preferAgentBrowser: true,
|
|
browserClient: browserClient,
|
|
},
|
|
memoryBackend
|
|
);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
try {
|
|
await browserClient.quit();
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
it('should test keyboard navigation on form page', async () => {
|
|
const result = await service.testKeyboardNavigation(TEST_PAGES.forms);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.value).toBeDefined();
|
|
|
|
const report = result.value!;
|
|
console.log('[Keyboard] Tab order items:', report.tabOrder?.length ?? 0);
|
|
console.log('[Keyboard] Issues found:', report.issues?.length ?? 0);
|
|
}, BROWSER_TIMEOUT);
|
|
});
|