initial wcag-ada
This commit is contained in:
parent
042b8cb83a
commit
d52cfe7de2
112 changed files with 9069 additions and 0 deletions
285
apps/wcag-ada/scanner/src/core/accessibility-browser.ts
Normal file
285
apps/wcag-ada/scanner/src/core/accessibility-browser.ts
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import { chromium, Browser, BrowserContext, Page } from 'playwright';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import type { AxeResults } from 'axe-core';
|
||||
import type {
|
||||
AccessibilityScanOptions,
|
||||
ViewportSize,
|
||||
AuthenticationConfig
|
||||
} from '@wcag-ada/shared';
|
||||
import { getWCAGTags, RESOURCE_BLOCK_PATTERNS } from '@wcag-ada/shared';
|
||||
import { getScannerConfig, type ScannerConfig } from '@wcag-ada/config';
|
||||
|
||||
export interface AccessibilityBrowserOptions {
|
||||
headless?: boolean;
|
||||
scanTimeout?: number;
|
||||
defaultViewport?: ViewportSize;
|
||||
blockResources?: boolean;
|
||||
}
|
||||
|
||||
export class AccessibilityBrowser {
|
||||
private browser?: Browser;
|
||||
private context?: BrowserContext;
|
||||
private logger: any;
|
||||
private config: ScannerConfig;
|
||||
private options: AccessibilityBrowserOptions;
|
||||
private initialized = false;
|
||||
|
||||
constructor(logger?: any, options?: AccessibilityBrowserOptions) {
|
||||
this.logger = logger || console;
|
||||
this.config = getScannerConfig();
|
||||
|
||||
// Merge config with provided options
|
||||
this.options = {
|
||||
headless: options?.headless ?? this.config.headless,
|
||||
scanTimeout: options?.scanTimeout ?? this.config.timeout,
|
||||
defaultViewport: options?.defaultViewport ?? this.config.viewport,
|
||||
blockResources: options?.blockResources ?? this.config.blockResources,
|
||||
};
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
this.browser = await chromium.launch({
|
||||
headless: this.options.headless,
|
||||
args: this.config.browsers.chromium.args,
|
||||
});
|
||||
|
||||
this.context = await this.browser.newContext({
|
||||
viewport: this.options.defaultViewport,
|
||||
});
|
||||
|
||||
// Block resources for faster scanning if enabled
|
||||
if (this.options.blockResources) {
|
||||
await this.context.route('**/*', (route) => {
|
||||
const url = route.request().url();
|
||||
const shouldBlock = RESOURCE_BLOCK_PATTERNS.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp(pattern.replace('*', '.*'));
|
||||
return regex.test(url);
|
||||
}
|
||||
return url.includes(pattern);
|
||||
});
|
||||
|
||||
if (shouldBlock) {
|
||||
route.abort();
|
||||
} else {
|
||||
route.continue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.context) await this.context.close();
|
||||
if (this.browser) await this.browser.close();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async scanPage(options: AccessibilityScanOptions): Promise<{
|
||||
page: Page;
|
||||
axeResults: AxeResults;
|
||||
html: string;
|
||||
}> {
|
||||
if (!this.context) throw new Error('Browser not initialized');
|
||||
|
||||
const { url, viewport, authenticate, wcagLevel, excludeSelectors, waitForSelector } = options;
|
||||
|
||||
const page = await this.context.newPage();
|
||||
|
||||
try {
|
||||
// Set viewport if different from default
|
||||
if (viewport) {
|
||||
await page.setViewportSize({
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle authentication if needed
|
||||
if (authenticate) {
|
||||
await this.handleAuthentication(page, authenticate);
|
||||
}
|
||||
|
||||
// Navigate to URL
|
||||
await page.goto(url, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: options.timeout || this.config.pageLoadTimeout,
|
||||
});
|
||||
|
||||
// Wait for specific selector if provided
|
||||
if (waitForSelector) {
|
||||
await page.waitForSelector(waitForSelector, {
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
// Additional wait for dynamic content
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Get page HTML
|
||||
const html = await page.content();
|
||||
|
||||
// Configure axe
|
||||
const axeBuilder = new AxeBuilder({ page });
|
||||
|
||||
// Set WCAG tags
|
||||
if (wcagLevel) {
|
||||
const tags = getWCAGTags(wcagLevel);
|
||||
axeBuilder.withTags(tags);
|
||||
}
|
||||
|
||||
// Exclude selectors if provided
|
||||
if (excludeSelectors && excludeSelectors.length > 0) {
|
||||
excludeSelectors.forEach(selector => {
|
||||
axeBuilder.exclude(selector);
|
||||
});
|
||||
}
|
||||
|
||||
// Set options
|
||||
axeBuilder.options({
|
||||
resultTypes: this.config.axe.resultTypes,
|
||||
elementRef: true,
|
||||
runOnly: {
|
||||
type: 'tag',
|
||||
values: wcagLevel ? getWCAGTags(wcagLevel) : this.config.axe.tags,
|
||||
},
|
||||
});
|
||||
|
||||
// Run accessibility scan
|
||||
const axeResults = await Promise.race([
|
||||
axeBuilder.analyze(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Scan timeout')), this.options.scanTimeout!)
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
page,
|
||||
axeResults,
|
||||
html,
|
||||
};
|
||||
} catch (error) {
|
||||
await page.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAuthentication(page: Page, auth: AuthenticationConfig): Promise<void> {
|
||||
switch (auth.type) {
|
||||
case 'basic':
|
||||
if (auth.credentials?.username && auth.credentials?.password) {
|
||||
await page.context().setHTTPCredentials({
|
||||
username: auth.credentials.username,
|
||||
password: auth.credentials.password,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'form':
|
||||
if (auth.loginUrl) {
|
||||
await page.goto(auth.loginUrl);
|
||||
|
||||
if (auth.selectors?.username && auth.credentials?.username) {
|
||||
await page.fill(auth.selectors.username, auth.credentials.username);
|
||||
}
|
||||
|
||||
if (auth.selectors?.password && auth.credentials?.password) {
|
||||
await page.fill(auth.selectors.password, auth.credentials.password);
|
||||
}
|
||||
|
||||
if (auth.selectors?.submit) {
|
||||
await page.click(auth.selectors.submit);
|
||||
await page.waitForNavigation({ waitUntil: 'networkidle' });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
if (auth.customScript) {
|
||||
await page.evaluate(auth.customScript);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async captureViolationScreenshots(
|
||||
page: Page,
|
||||
violations: AxeResults['violations']
|
||||
): Promise<Map<string, string>> {
|
||||
const screenshots = new Map<string, string>();
|
||||
|
||||
for (const violation of violations) {
|
||||
for (let i = 0; i < violation.nodes.length; i++) {
|
||||
const node = violation.nodes[i];
|
||||
if (node.target && node.target.length > 0) {
|
||||
try {
|
||||
const selector = node.target[0];
|
||||
const element = await page.$(selector);
|
||||
|
||||
if (element) {
|
||||
const screenshot = await element.screenshot({
|
||||
type: 'png',
|
||||
});
|
||||
|
||||
const key = `${violation.id}_${i}`;
|
||||
screenshots.set(key, screenshot.toString('base64'));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger?.warn(`Failed to capture screenshot for ${violation.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return screenshots;
|
||||
}
|
||||
|
||||
async evaluateCustomRules(page: Page, customRules: any[]): Promise<any[]> {
|
||||
const results = [];
|
||||
|
||||
for (const rule of customRules) {
|
||||
try {
|
||||
const elements = await page.$$(rule.selector);
|
||||
const violations = [];
|
||||
|
||||
for (const element of elements) {
|
||||
const passes = await page.evaluate(
|
||||
(el, validatorStr) => {
|
||||
const validator = new Function('element', `return (${validatorStr})(element)`);
|
||||
return validator(el);
|
||||
},
|
||||
element,
|
||||
rule.validator.toString()
|
||||
);
|
||||
|
||||
if (!passes) {
|
||||
const html = await element.evaluate(el => el.outerHTML);
|
||||
violations.push({
|
||||
html,
|
||||
target: [rule.selector],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
results.push({
|
||||
id: rule.id,
|
||||
description: rule.description,
|
||||
help: rule.help,
|
||||
helpUrl: rule.helpUrl,
|
||||
impact: rule.severity,
|
||||
tags: rule.tags,
|
||||
nodes: violations,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger?.error(`Failed to evaluate custom rule ${rule.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
303
apps/wcag-ada/scanner/src/core/scanner.ts
Normal file
303
apps/wcag-ada/scanner/src/core/scanner.ts
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import { AccessibilityBrowser } from './accessibility-browser';
|
||||
import type {
|
||||
AccessibilityScanOptions,
|
||||
AccessibilityScanResult,
|
||||
AccessibilityViolation,
|
||||
ScanSummary,
|
||||
PageMetadata,
|
||||
WCAGCompliance,
|
||||
WCAGLevel,
|
||||
CriteriaResult
|
||||
} from '@wcag-ada/shared';
|
||||
import {
|
||||
calculateComplianceScore,
|
||||
summarizeViolations,
|
||||
generateFixSuggestion,
|
||||
extractPageMetadata,
|
||||
isCompliantWithLevel,
|
||||
CRITICAL_WCAG_CRITERIA
|
||||
} from '@wcag-ada/shared';
|
||||
import type { AxeResults, Result as AxeResult } from 'axe-core';
|
||||
import { initializeWcagConfig } from '@wcag-ada/config';
|
||||
|
||||
export class AccessibilityScanner {
|
||||
private browser: AccessibilityBrowser;
|
||||
private logger: any;
|
||||
|
||||
constructor(logger?: any) {
|
||||
this.logger = logger || console;
|
||||
|
||||
// Initialize config for scanner service
|
||||
initializeWcagConfig();
|
||||
|
||||
this.browser = new AccessibilityBrowser(logger);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.browser.initialize();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.browser.close();
|
||||
}
|
||||
|
||||
async scan(options: AccessibilityScanOptions): Promise<AccessibilityScanResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Perform the scan
|
||||
const { page, axeResults, html } = await this.browser.scanPage(options);
|
||||
|
||||
// Capture screenshots if requested
|
||||
let screenshotMap: Map<string, string> | undefined;
|
||||
if (options.includeScreenshots) {
|
||||
screenshotMap = await this.browser.captureViolationScreenshots(page, axeResults.violations);
|
||||
}
|
||||
|
||||
// Evaluate custom rules if provided
|
||||
let customViolations: AxeResult[] = [];
|
||||
if (options.customRules && options.customRules.length > 0) {
|
||||
customViolations = await this.browser.evaluateCustomRules(page, options.customRules);
|
||||
}
|
||||
|
||||
// Close the page
|
||||
await page.close();
|
||||
|
||||
// Process results
|
||||
const violations = this.processViolations(
|
||||
[...axeResults.violations, ...customViolations],
|
||||
screenshotMap
|
||||
);
|
||||
|
||||
// Extract metadata
|
||||
const pageMetadata = this.extractPageMetadata(html, options);
|
||||
|
||||
// Calculate summary
|
||||
const summary = this.calculateSummary(axeResults, violations);
|
||||
|
||||
// Determine WCAG compliance
|
||||
const wcagCompliance = this.calculateWCAGCompliance(
|
||||
violations,
|
||||
axeResults.passes,
|
||||
options.wcagLevel || { level: 'AA', version: '2.1' },
|
||||
summary.score
|
||||
);
|
||||
|
||||
const scanDuration = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
url: options.url,
|
||||
timestamp: new Date(),
|
||||
scanDuration,
|
||||
summary,
|
||||
violations,
|
||||
passes: axeResults.passes,
|
||||
incomplete: axeResults.incomplete,
|
||||
inapplicable: axeResults.inapplicable,
|
||||
pageMetadata,
|
||||
wcagCompliance,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Scan failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private processViolations(
|
||||
axeViolations: AxeResult[],
|
||||
screenshots?: Map<string, string>
|
||||
): AccessibilityViolation[] {
|
||||
return axeViolations.map(violation => {
|
||||
const nodes = violation.nodes.map((node, index) => {
|
||||
const screenshotKey = `${violation.id}_${index}`;
|
||||
return {
|
||||
html: node.html,
|
||||
target: node.target as string[],
|
||||
xpath: node.xpath?.[0],
|
||||
failureSummary: node.failureSummary,
|
||||
screenshot: screenshots?.get(screenshotKey),
|
||||
relatedNodes: node.relatedNodes?.map(related => ({
|
||||
html: related.html,
|
||||
target: related.target as string[],
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: violation.id,
|
||||
impact: violation.impact!,
|
||||
description: violation.description,
|
||||
help: violation.help,
|
||||
helpUrl: violation.helpUrl,
|
||||
tags: violation.tags,
|
||||
nodes,
|
||||
wcagCriteria: this.mapToWCAGCriteria(violation.tags),
|
||||
fixSuggestion: generateFixSuggestion(violation.id),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private extractPageMetadata(html: string, options: AccessibilityScanOptions): PageMetadata {
|
||||
const metadata = extractPageMetadata(html);
|
||||
|
||||
return {
|
||||
title: metadata.title || 'Untitled Page',
|
||||
url: options.url,
|
||||
viewport: options.viewport || { width: 1280, height: 720 },
|
||||
language: metadata.language,
|
||||
doctype: this.extractDoctype(html),
|
||||
hasLandmarks: metadata.hasLandmarks || false,
|
||||
hasHeadings: metadata.hasHeadings || false,
|
||||
imagesCount: metadata.imagesCount || 0,
|
||||
formsCount: metadata.formsCount || 0,
|
||||
linksCount: metadata.linksCount || 0,
|
||||
};
|
||||
}
|
||||
|
||||
private extractDoctype(html: string): string {
|
||||
const doctypeMatch = html.match(/<!DOCTYPE\s+([^>]+)>/i);
|
||||
return doctypeMatch ? doctypeMatch[1].trim() : 'unknown';
|
||||
}
|
||||
|
||||
private calculateSummary(
|
||||
axeResults: AxeResults,
|
||||
violations: AccessibilityViolation[]
|
||||
): ScanSummary {
|
||||
const violationSummary = summarizeViolations(violations);
|
||||
const violationCount = violations.reduce((sum, v) => sum + v.nodes.length, 0);
|
||||
|
||||
const result: AccessibilityScanResult = {
|
||||
violations,
|
||||
passes: axeResults.passes,
|
||||
summary: {} as ScanSummary,
|
||||
} as AccessibilityScanResult;
|
||||
|
||||
const score = calculateComplianceScore(result);
|
||||
|
||||
return {
|
||||
violationCount,
|
||||
passCount: axeResults.passes.length,
|
||||
incompleteCount: axeResults.incomplete.length,
|
||||
inapplicableCount: axeResults.inapplicable.length,
|
||||
...violationSummary,
|
||||
score,
|
||||
} as ScanSummary;
|
||||
}
|
||||
|
||||
private calculateWCAGCompliance(
|
||||
violations: AccessibilityViolation[],
|
||||
passes: AxeResult[],
|
||||
wcagLevel: WCAGLevel,
|
||||
score: number
|
||||
): WCAGCompliance {
|
||||
const criteriaMap = new Map<string, CriteriaResult>();
|
||||
|
||||
// Initialize critical criteria
|
||||
CRITICAL_WCAG_CRITERIA.forEach(criterion => {
|
||||
criteriaMap.set(criterion, {
|
||||
criterion,
|
||||
level: this.getCriterionLevel(criterion),
|
||||
passed: true,
|
||||
violations: [],
|
||||
});
|
||||
});
|
||||
|
||||
// Process violations
|
||||
violations.forEach(violation => {
|
||||
violation.wcagCriteria?.forEach(criterion => {
|
||||
if (!criteriaMap.has(criterion)) {
|
||||
criteriaMap.set(criterion, {
|
||||
criterion,
|
||||
level: this.getCriterionLevel(criterion),
|
||||
passed: false,
|
||||
violations: [violation.id],
|
||||
});
|
||||
} else {
|
||||
const result = criteriaMap.get(criterion)!;
|
||||
result.passed = false;
|
||||
result.violations.push(violation.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const criteriaResults = Array.from(criteriaMap.values());
|
||||
const isCompliant = isCompliantWithLevel(score, wcagLevel.level);
|
||||
|
||||
return {
|
||||
level: wcagLevel,
|
||||
isCompliant,
|
||||
criteriaResults,
|
||||
overallScore: score,
|
||||
};
|
||||
}
|
||||
|
||||
private mapToWCAGCriteria(tags: string[]): string[] {
|
||||
const criteriaMap: Record<string, string[]> = {
|
||||
'wcag111': ['1.1.1'],
|
||||
'wcag131': ['1.3.1'],
|
||||
'wcag141': ['1.4.1'],
|
||||
'wcag143': ['1.4.3'],
|
||||
'wcag211': ['2.1.1'],
|
||||
'wcag212': ['2.1.2'],
|
||||
'wcag241': ['2.4.1'],
|
||||
'wcag242': ['2.4.2'],
|
||||
'wcag243': ['2.4.3'],
|
||||
'wcag244': ['2.4.4'],
|
||||
'wcag311': ['3.1.1'],
|
||||
'wcag331': ['3.3.1'],
|
||||
'wcag332': ['3.3.2'],
|
||||
'wcag411': ['4.1.1'],
|
||||
'wcag412': ['4.1.2'],
|
||||
};
|
||||
|
||||
const criteria = new Set<string>();
|
||||
tags.forEach(tag => {
|
||||
const mapped = criteriaMap[tag];
|
||||
if (mapped) {
|
||||
mapped.forEach(c => criteria.add(c));
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(criteria);
|
||||
}
|
||||
|
||||
private getCriterionLevel(criterion: string): 'A' | 'AA' | 'AAA' {
|
||||
const levelMap: Record<string, 'A' | 'AA' | 'AAA'> = {
|
||||
'1.1.1': 'A',
|
||||
'1.3.1': 'A',
|
||||
'1.4.1': 'A',
|
||||
'1.4.3': 'AA',
|
||||
'2.1.1': 'A',
|
||||
'2.1.2': 'A',
|
||||
'2.4.1': 'A',
|
||||
'2.4.2': 'A',
|
||||
'2.4.3': 'A',
|
||||
'2.4.4': 'A',
|
||||
'3.1.1': 'A',
|
||||
'3.3.1': 'A',
|
||||
'3.3.2': 'A',
|
||||
'4.1.1': 'A',
|
||||
'4.1.2': 'A',
|
||||
};
|
||||
|
||||
return levelMap[criterion] || 'A';
|
||||
}
|
||||
|
||||
async scanMultiplePages(
|
||||
urls: string[],
|
||||
baseOptions: Omit<AccessibilityScanOptions, 'url'>
|
||||
): Promise<AccessibilityScanResult[]> {
|
||||
const results: AccessibilityScanResult[] = [];
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const result = await this.scan({ ...baseOptions, url });
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to scan ${url}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
79
apps/wcag-ada/scanner/src/example.ts
Normal file
79
apps/wcag-ada/scanner/src/example.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { AccessibilityScanner } from './core/scanner';
|
||||
import type { AccessibilityScanOptions } from '@wcag-ada/shared';
|
||||
import { createLogger } from '@lib/service/core-logger';
|
||||
|
||||
const logger = createLogger('scanner-example');
|
||||
|
||||
async function runExample() {
|
||||
const scanner = new AccessibilityScanner();
|
||||
|
||||
try {
|
||||
// Initialize the browser
|
||||
await scanner.initialize();
|
||||
|
||||
// Define scan options
|
||||
const scanOptions: AccessibilityScanOptions = {
|
||||
url: 'https://example.com',
|
||||
wcagLevel: {
|
||||
level: 'AA',
|
||||
version: '2.1'
|
||||
},
|
||||
includeScreenshots: true,
|
||||
viewport: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
},
|
||||
excludeSelectors: [
|
||||
'.cookie-banner',
|
||||
'[aria-hidden="true"]'
|
||||
]
|
||||
};
|
||||
|
||||
logger.info('Starting accessibility scan...');
|
||||
const result = await scanner.scan(scanOptions);
|
||||
|
||||
// Display summary
|
||||
logger.info('\n=== SCAN SUMMARY ===');
|
||||
logger.info(`URL: ${result.url}`);
|
||||
logger.info(`Compliance Score: ${result.summary.score}%`);
|
||||
logger.info(`WCAG ${result.wcagCompliance.level.version} Level ${result.wcagCompliance.level.level}: ${result.wcagCompliance.isCompliant ? 'PASSED' : 'FAILED'}`);
|
||||
logger.info(`\nViolations: ${result.summary.violationCount}`);
|
||||
logger.info(`- Critical: ${result.summary.criticalIssues}`);
|
||||
logger.info(`- Serious: ${result.summary.seriousIssues}`);
|
||||
logger.info(`- Moderate: ${result.summary.moderateIssues}`);
|
||||
logger.info(`- Minor: ${result.summary.minorIssues}`);
|
||||
|
||||
// Display top violations
|
||||
if (result.violations.length > 0) {
|
||||
logger.info('\n=== TOP VIOLATIONS ===');
|
||||
result.violations.slice(0, 5).forEach((violation, index) => {
|
||||
logger.info(`\n${index + 1}. ${violation.help}`);
|
||||
logger.info(` Impact: ${violation.impact}`);
|
||||
logger.info(` Affected elements: ${violation.nodes.length}`);
|
||||
logger.info(` Fix: ${violation.fixSuggestion}`);
|
||||
logger.info(` More info: ${violation.helpUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Display page metadata
|
||||
logger.info('\n=== PAGE METADATA ===');
|
||||
logger.info(`Title: ${result.pageMetadata.title}`);
|
||||
logger.info(`Language: ${result.pageMetadata.language || 'Not specified'}`);
|
||||
logger.info(`Images: ${result.pageMetadata.imagesCount}`);
|
||||
logger.info(`Forms: ${result.pageMetadata.formsCount}`);
|
||||
logger.info(`Links: ${result.pageMetadata.linksCount}`);
|
||||
logger.info(`Has landmarks: ${result.pageMetadata.hasLandmarks ? 'Yes' : 'No'}`);
|
||||
logger.info(`Has headings: ${result.pageMetadata.hasHeadings ? 'Yes' : 'No'}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Scan failed:', error);
|
||||
} finally {
|
||||
// Always close the browser
|
||||
await scanner.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the example
|
||||
if (require.main === module) {
|
||||
runExample().catch((error) => logger.error('Example failed:', error));
|
||||
}
|
||||
14
apps/wcag-ada/scanner/src/index.ts
Normal file
14
apps/wcag-ada/scanner/src/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export { AccessibilityScanner } from './core/scanner';
|
||||
export { AccessibilityBrowser } from './core/accessibility-browser';
|
||||
export type { AccessibilityBrowserOptions } from './core/accessibility-browser';
|
||||
|
||||
// Re-export shared types for convenience
|
||||
export type {
|
||||
AccessibilityScanOptions,
|
||||
AccessibilityScanResult,
|
||||
AccessibilityViolation,
|
||||
WCAGLevel,
|
||||
Website,
|
||||
ScanJob,
|
||||
ComplianceReport,
|
||||
} from '@wcag-ada/shared';
|
||||
Loading…
Add table
Add a link
Reference in a new issue