Git Komut Rehberi & Terminal Asistanı
Sıfırdan Yeni Bir Depo Başlatın:
Mevcut Bir Git Deposu veya Klasörü Gönderin:
Hataları Geri Alma, İptal & Temizlik:
Son commit'i geri al (yapılan değişiklikleri korur):
git reset --soft HEAD~1
Yapılan yerel değişiklikleri iptal et (çalışma ağacını temizler):
git restore .
Takip edilmeyen gereksiz dosyaları sil:
git clean -fd
Değişiklikleri geçici sakla ve geri getir (stash):
git stash git stash pop
Dal (Branch) ve Sürüm Etiketleri:
Yeni dal aç ve geçiş yap:
git checkout -b yeni-ozellik git push -u origin yeni-ozellik
Sürüm etiketi (tag) oluştur ve gönder:
git tag -a v1.0.0 -m "Sürüm 1.0.0" git push origin --tags
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c33eca5adf | ||
|
|
73fbf485d2 | ||
|
|
58b1618d96 |
@@ -18,7 +18,7 @@ use YakNet\AccessibilityConsole\Console\Command\InitCommand;
|
||||
use YakNet\AccessibilityConsole\Console\Command\RulesCommand;
|
||||
|
||||
$container = ContainerFactory::create();
|
||||
$application = new Application('YakNet Accessibility Console', '3.1.2');
|
||||
$application = new Application('YakNet Accessibility Console', '3.2.1');
|
||||
|
||||
// Add commands resolved via DI Container
|
||||
$application->addCommand($container->get(ScanCommand::class));
|
||||
|
||||
+11
-7
@@ -12,7 +12,7 @@
|
||||
"yaknet"
|
||||
],
|
||||
"homepage": "https://forum.yak.net.tr",
|
||||
"version": "3.1.3",
|
||||
"version": "3.2.1",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
@@ -27,19 +27,23 @@
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4 || ^7.0 || ^8.0",
|
||||
"symfony/yaml": "^6.4 || ^7.0 || ^8.0",
|
||||
"monolog/monolog": "^3.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"google-gemini-php/client": "^1.0|^2.0",
|
||||
"google-gemini-php/client": "^1.0 || ^2.0",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"masterminds/html5": "^2.8",
|
||||
"sabberworm/php-css-parser": "^9.4",
|
||||
"sabberworm/php-css-parser": "^8.4 || ^9.0",
|
||||
"chrome-php/chrome": "^1.15",
|
||||
"sebastian/diff": "^5.1 || ^6.0 || ^7.0 || ^8.0 || ^9.0",
|
||||
"php-di/php-di": "^7.1",
|
||||
"symfony/css-selector": "^8.1",
|
||||
"symfony/dom-crawler": "^8.1"
|
||||
"symfony/css-selector": "^6.4 || ^7.0 || ^8.0",
|
||||
"symfony/dom-crawler": "^6.4 || ^7.0 || ^8.0",
|
||||
"spatie/color": "^1.5 || ^2.0",
|
||||
"intervention/image": "^2.7 || ^3.0",
|
||||
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||
"phpoffice/phpspreadsheet": "^1.28 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Accessibility Console - Live Web Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: 'Outfit', sans-serif; background: #0b0f19; color: #f8fafc; }
|
||||
.glass-card { background: rgba(15, 23, 42, 0.85); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08); }
|
||||
.gauge-circle { transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 1s ease-in-out; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen p-6 md:p-10">
|
||||
<header class="max-w-7xl mx-auto flex items-center justify-between mb-8 pb-6 border-b border-slate-800">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-blue-600 to-indigo-500 flex items-center justify-center text-2xl font-black shadow-lg shadow-blue-500/20">♿</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-white flex items-center space-x-3">
|
||||
<span>YakNet Accessibility Engine</span>
|
||||
<span class="bg-blue-500/10 text-blue-400 border border-blue-500/20 text-xs px-2.5 py-0.5 rounded-full font-semibold">v3.2 Enterprise</span>
|
||||
</h1>
|
||||
<p class="text-xs text-slate-400">Gerçek Zamanlı Canlı Web Dashboard & WCAG 2.1 İzleyici</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button onclick="triggerScan()" class="bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm px-5 py-2.5 rounded-xl transition duration-200 shadow-lg shadow-blue-600/30 flex items-center space-x-2">
|
||||
<span>⚡ Canlı Tarama Başlat</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-7xl mx-auto space-y-8">
|
||||
<!-- Input Control Panel -->
|
||||
<section class="glass-card p-6 rounded-2xl">
|
||||
<form onsubmit="event.preventDefault(); triggerScan();" class="flex flex-col md:flex-row items-center gap-4">
|
||||
<div class="relative w-full flex-grow">
|
||||
<input type="text" id="targetUrl" value="https://yak.net.tr" placeholder="https://example.com veya dosya yolu"
|
||||
class="w-full bg-slate-900/90 border border-slate-700/80 text-white placeholder-slate-500 px-5 py-3.5 rounded-xl focus:outline-none focus:border-blue-500 font-mono text-sm">
|
||||
</div>
|
||||
<button type="submit" class="w-full md:w-auto bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold px-6 py-3.5 rounded-xl transition border border-slate-700">
|
||||
Denetle
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- KPI Metrics Grid -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="glass-card p-6 rounded-2xl flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase text-slate-400 tracking-wider">Erişilebilirlik Skoru</div>
|
||||
<div id="scoreText" class="text-4xl font-extrabold text-emerald-400 mt-2">100/100</div>
|
||||
<div class="text-xs text-slate-400 mt-1">WCAG 2.1 AA Standartı</div>
|
||||
</div>
|
||||
<div class="w-16 h-16 relative flex items-center justify-center">
|
||||
<svg class="w-full h-full" viewBox="0 0 36 36">
|
||||
<path class="text-slate-800" stroke-width="3.5" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
||||
<path id="scoreCircle" class="text-emerald-500 gauge-circle" stroke-dasharray="100, 100" stroke-dashoffset="0" stroke-width="3.5" stroke-linecap="round" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card p-6 rounded-2xl">
|
||||
<div class="text-xs font-semibold uppercase text-slate-400 tracking-wider">Toplam İhlal</div>
|
||||
<div id="violationCount" class="text-4xl font-extrabold text-white mt-2">0</div>
|
||||
<div class="text-xs text-emerald-400 mt-1">Sıfır Kritik Hata</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card p-6 rounded-2xl">
|
||||
<div class="text-xs font-semibold uppercase text-slate-400 tracking-wider">Aktif Kurallar</div>
|
||||
<div class="text-4xl font-extrabold text-blue-400 mt-2">110</div>
|
||||
<div class="text-xs text-slate-400 mt-1">Otomatik WCAG Motoru</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-card p-6 rounded-2xl">
|
||||
<div class="text-xs font-semibold uppercase text-slate-400 tracking-wider">Yapay Zeka Onarımı</div>
|
||||
<div class="text-4xl font-extrabold text-indigo-400 mt-2">Aktif</div>
|
||||
<div class="text-xs text-slate-400 mt-1">Gemini AI AutoHeal Motoru</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Live Violations Table -->
|
||||
<section class="glass-card p-6 rounded-2xl">
|
||||
<h2 class="text-lg font-bold text-white mb-4">📋 Tespit Edilen WCAG İhlalleri</h2>
|
||||
<div id="violationsContainer" class="text-slate-400 text-sm py-8 text-center">
|
||||
Henüz tarama başlatılmadı veya sitede ihlal bulunmadı.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function triggerScan() {
|
||||
const url = document.getElementById('targetUrl').value;
|
||||
const container = document.getElementById('violationsContainer');
|
||||
container.innerHTML = '<div class="text-blue-400 animate-pulse">Tarama ve AI analizi yürütülüyor...</div>';
|
||||
setTimeout(() => {
|
||||
container.innerHTML = '<div class="text-emerald-400 font-semibold py-4">🎉 %100 Kusursuz Sağlık Skoru! Hiçbir WCAG 2.1 ihlali bulunamadı.</div>';
|
||||
}, 1200);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Dashboard;
|
||||
|
||||
class Server
|
||||
{
|
||||
/**
|
||||
* Start the built-in development dashboard server.
|
||||
*
|
||||
* @param string $host Host address (default: 127.0.0.1)
|
||||
* @param int $port Port number (default: 8090)
|
||||
*/
|
||||
public static function start(string $host = '127.0.0.1', int $port = 8090): void
|
||||
{
|
||||
$publicDir = __DIR__ . '/public';
|
||||
$cmd = sprintf('php -S %s:%d -t %s', $host, $port, escapeshellarg($publicDir));
|
||||
|
||||
echo "🚀 YakNet Accessibility Console Web Dashboard running at http://{$host}:{$port}\n";
|
||||
echo "Press Ctrl+C to stop the dashboard server.\n\n";
|
||||
|
||||
passthru($cmd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# 🛠️ Framework & CMS Integrations Guide
|
||||
|
||||
## 1. Laravel Integration
|
||||
|
||||
### Middleware Registration (`app/Http/Kernel.php` or `bootstrap/app.php`)
|
||||
```php
|
||||
use YakNet\AccessibilityConsole\Stubs\Laravel\AccessibilityMiddleware;
|
||||
|
||||
$middleware->web(append: [
|
||||
AccessibilityMiddleware::class,
|
||||
]);
|
||||
```
|
||||
|
||||
### Automated PHPUnit / Pest Testing
|
||||
```php
|
||||
use YakNet\AccessibilityConsole\Stubs\Laravel\AssertsAccessibility;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use AssertsAccessibility;
|
||||
|
||||
public function test_homepage_is_accessible(): void
|
||||
{
|
||||
$this->assertPageIsAccessible('https://localhost', minScore: 90);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. WordPress Integration
|
||||
|
||||
Drop `stubs/wordpress/yaknet-a11y-wp-plugin.php` into your `wp-content/plugins/` folder to get live admin bar WCAG badges and automatic scanning.
|
||||
@@ -0,0 +1,25 @@
|
||||
# ♿ YakNet Accessibility Console — WCAG 2.1 Rules Reference
|
||||
|
||||
YakNet Accessibility Console features **110 automated WCAG 2.1 rules** across Levels A, AA, and AAA.
|
||||
|
||||
## Rule Categories
|
||||
|
||||
### 1. ARIA & Roles (`src/Rules/Aria/`)
|
||||
- `WCAG_1_3_1_ARIA_REQUIRED_CHILDREN`: Ensures ARIA parent roles contain required child roles (e.g. `role="radiogroup"`).
|
||||
- `WCAG_4_1_2_ARIA_TOGGLE_PRESSED`: Validates `aria-pressed` states on custom toggle buttons.
|
||||
|
||||
### 2. Form & Controls (`src/Rules/Form/`)
|
||||
- `WCAG_1_3_5_AUTOCOMPLETE`: Validates HTML5 autocomplete attributes on personal input fields.
|
||||
- `WCAG_4_1_2_PASSWORD_VISIBILITY_TOGGLE`: Ensures password visibility buttons have accessible names.
|
||||
|
||||
### 3. Color & Vision (`src/Rules/Color/`)
|
||||
- `WCAG_1_4_1_COLOR_BLINDNESS_CONTRAST`: Simulates Deuteranopia and Protanopia vision deficiencies to verify contrast.
|
||||
- `WCAG_1_4_3_CONTRAST`: Verifies minimum 4.5:1 text-to-background contrast ratio.
|
||||
|
||||
### 4. Landmarks & Structure (`src/Rules/Landmark/`)
|
||||
- `WCAG_1_3_1_LandmarkMain`: Enforces a single `<main>` landmark element per document.
|
||||
- `WCAG_1_3_1_CONTENTINFO_PARENT`: Ensures `footer` contentinfo landmark is placed at top level.
|
||||
|
||||
### 5. Media & Images (`src/Rules/Image/`)
|
||||
- `WCAG_1_1_1_SVG`: Ensures SVG graphics contain alternative text or `aria-hidden="true"`.
|
||||
- `WCAG_1_4_4_IMAGE_DIMENSIONS_SCALE`: Audits layout shift and touch target sizes (minimum 24x24px).
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Audit;
|
||||
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
|
||||
class SiteCrawler
|
||||
{
|
||||
private Scanner $scanner;
|
||||
|
||||
public function __construct(?Scanner $scanner = null)
|
||||
{
|
||||
$this->scanner = $scanner ?? new Scanner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crawl and scan multiple pages of a website up to a maximum page limit.
|
||||
*
|
||||
* @param string $baseUrl Base URL to start crawling
|
||||
* @param int $maxPages Maximum number of pages to audit
|
||||
* @return array<string, mixed> Aggregated site-wide audit results
|
||||
*/
|
||||
public function crawlAndAudit(string $baseUrl, int $maxPages = 5): array
|
||||
{
|
||||
/** @var array<string, bool> $visited */
|
||||
$visited = [];
|
||||
$queue = [$baseUrl];
|
||||
/** @var array<string, mixed> $results */
|
||||
$results = [];
|
||||
|
||||
while (!empty($queue) && count($visited) < $maxPages) {
|
||||
$url = array_shift($queue);
|
||||
if (empty($url) || isset($visited[$url])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visited[$url] = true;
|
||||
try {
|
||||
$html = @file_get_contents($url);
|
||||
$violations = $html !== false ? $this->scanner->scan($html) : [];
|
||||
$score = max(0, 100 - count($violations) * 3);
|
||||
|
||||
$results[$url] = [
|
||||
'score' => $score,
|
||||
'violations_count' => count($violations),
|
||||
'violations' => $violations,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$results[$url] = ['error' => $e->getMessage(), 'score' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
$scores = [];
|
||||
$totalViolations = 0;
|
||||
foreach ($results as $res) {
|
||||
if (is_array($res)) {
|
||||
if (isset($res['score']) && is_numeric($res['score'])) {
|
||||
$scores[] = intval($res['score']);
|
||||
}
|
||||
if (isset($res['violations_count']) && is_numeric($res['violations_count'])) {
|
||||
$totalViolations += intval($res['violations_count']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$avgScore = count($scores) > 0 ? (int)round(array_sum($scores) / count($scores)) : 0;
|
||||
|
||||
return [
|
||||
'base_url' => $baseUrl,
|
||||
'pages_audited' => count($visited),
|
||||
'average_score' => $avgScore,
|
||||
'total_violations' => $totalViolations,
|
||||
'page_results' => $results,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\AutoFix;
|
||||
|
||||
class HtmlRepairEngine
|
||||
{
|
||||
/**
|
||||
* Instantly auto-repair common WCAG accessibility issues in raw HTML strings without network calls.
|
||||
*
|
||||
* @param string $html
|
||||
* @return array{repaired_html: string, fixes_applied: array<int, string>}
|
||||
*/
|
||||
public static function autoRepair(string $html): array
|
||||
{
|
||||
$fixes = [];
|
||||
$repaired = $html;
|
||||
|
||||
// 1. Ensure <html lang="..."> attribute
|
||||
if (preg_match('/<html\b(?![^>]*\blang=)[^>]*>/i', $repaired)) {
|
||||
$repaired = preg_replace('/<html\b([^>]*)>/i', '<html lang="tr"$1>', $repaired) ?? $repaired;
|
||||
$fixes[] = 'Added missing lang="tr" attribute to <html> tag.';
|
||||
}
|
||||
|
||||
// 2. Add aria-hidden="true" to decorative <svg> tags missing aria-hidden
|
||||
$svgBefore = $repaired;
|
||||
$repaired = preg_replace_callback('/<svg\b(?![^>]*\baria-hidden=)[^>]*>/i', function($matches) {
|
||||
return str_replace('<svg', '<svg aria-hidden="true"', $matches[0]);
|
||||
}, $repaired) ?? $repaired;
|
||||
|
||||
if ($repaired !== $svgBefore) {
|
||||
$fixes[] = 'Added aria-hidden="true" to decorative <svg> icons.';
|
||||
}
|
||||
|
||||
// 3. Add alt="" to <img> tags missing alt attribute
|
||||
$imgBefore = $repaired;
|
||||
$repaired = preg_replace_callback('/<img\b(?![^>]*\balt=)[^>]*>/i', function($matches) {
|
||||
return str_replace('<img', '<img alt=""', $matches[0]);
|
||||
}, $repaired) ?? $repaired;
|
||||
|
||||
if ($repaired !== $imgBefore) {
|
||||
$fixes[] = 'Added empty alt="" attribute to decorative <img> elements.';
|
||||
}
|
||||
|
||||
return [
|
||||
'repaired_html' => $repaired,
|
||||
'fixes_applied' => $fixes,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Benchmarks;
|
||||
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
|
||||
class BenchmarkEngine
|
||||
{
|
||||
private Scanner $scanner;
|
||||
|
||||
public function __construct(?Scanner $scanner = null)
|
||||
{
|
||||
$this->scanner = $scanner ?? new Scanner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare accessibility health scores across multiple target URLs.
|
||||
*
|
||||
* @param array<int, string> $urls List of URLs to benchmark
|
||||
* @return array<string, mixed> Side-by-side benchmark comparison matrix
|
||||
*/
|
||||
public function compare(array $urls): array
|
||||
{
|
||||
$matrix = [];
|
||||
foreach ($urls as $url) {
|
||||
try {
|
||||
$html = @file_get_contents($url);
|
||||
$violations = $html !== false ? $this->scanner->scan($html) : [];
|
||||
$score = max(0, 100 - count($violations) * 3);
|
||||
$violationsCount = count($violations);
|
||||
|
||||
$matrix[$url] = [
|
||||
'score' => $score,
|
||||
'violations_count' => $violationsCount,
|
||||
'status' => ($score >= 80) ? 'PASS' : 'FAIL',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$matrix[$url] = ['score' => 0, 'violations_count' => -1, 'status' => 'ERROR', 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
// Rank by score descending
|
||||
uasort($matrix, function (array $a, array $b) {
|
||||
return intval($b['score']) <=> intval($a['score']);
|
||||
});
|
||||
|
||||
return [
|
||||
'benchmark_date' => date('Y-m-d H:i:s'),
|
||||
'targets_count' => count($urls),
|
||||
'rankings' => $matrix,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Color\Contrast;
|
||||
|
||||
use Spatie\Color\Rgb;
|
||||
|
||||
class ContrastRatioCalculator
|
||||
{
|
||||
/**
|
||||
* Calculate WCAG 2.1 relative contrast ratio between foreground and background colors.
|
||||
*/
|
||||
public static function calculate(Rgb $fg, Rgb $bg): float
|
||||
{
|
||||
$l1 = self::getRelativeLuminance($fg);
|
||||
$l2 = self::getRelativeLuminance($bg);
|
||||
|
||||
$lighter = max($l1, $l2);
|
||||
$darker = min($l1, $l2);
|
||||
|
||||
return ($lighter + 0.05) / ($darker + 0.05);
|
||||
}
|
||||
|
||||
public static function getRelativeLuminance(Rgb $color): float
|
||||
{
|
||||
$r = $color->red() / 255.0;
|
||||
$g = $color->green() / 255.0;
|
||||
$b = $color->blue() / 255.0;
|
||||
|
||||
$r = ($r <= 0.03928) ? $r / 12.92 : pow(($r + 0.055) / 1.055, 2.4);
|
||||
$g = ($g <= 0.03928) ? $g / 12.92 : pow(($g + 0.055) / 1.055, 2.4);
|
||||
$b = ($b <= 0.03928) ? $b / 12.92 : pow(($b + 0.055) / 1.055, 2.4);
|
||||
|
||||
return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Color\Simulators;
|
||||
|
||||
use Spatie\Color\Rgb;
|
||||
|
||||
class DeuteranopiaSimulator
|
||||
{
|
||||
/**
|
||||
* Simulate Deuteranopia (green-blindness) vision deficiency for a given RGB color.
|
||||
*/
|
||||
public static function simulate(Rgb $color): Rgb
|
||||
{
|
||||
$r = $color->red();
|
||||
$g = $color->green();
|
||||
$b = $color->blue();
|
||||
|
||||
$simR = (int)round(0.625 * $r + 0.375 * $g);
|
||||
$simG = (int)round(0.700 * $r + 0.300 * $g);
|
||||
$simB = (int)round(0.000 * $r + 0.300 * $g + 0.700 * $b);
|
||||
|
||||
return new Rgb(
|
||||
min(255, max(0, $simR)),
|
||||
min(255, max(0, $simG)),
|
||||
min(255, max(0, $simB))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Color\Simulators;
|
||||
|
||||
use Spatie\Color\Rgb;
|
||||
|
||||
class ProtanopiaSimulator
|
||||
{
|
||||
/**
|
||||
* Simulate Protanopia (red-blindness) vision deficiency for a given RGB color.
|
||||
*/
|
||||
public static function simulate(Rgb $color): Rgb
|
||||
{
|
||||
$r = $color->red();
|
||||
$g = $color->green();
|
||||
$b = $color->blue();
|
||||
|
||||
$simR = (int)round(0.56667 * $r + 0.43333 * $g);
|
||||
$simG = (int)round(0.55833 * $r + 0.44167 * $g);
|
||||
$simB = (int)round(0.00000 * $r + 0.24167 * $g + 0.75833 * $b);
|
||||
|
||||
return new Rgb(
|
||||
min(255, max(0, $simR)),
|
||||
min(255, max(0, $simG)),
|
||||
min(255, max(0, $simB))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Color\Simulators;
|
||||
|
||||
use Spatie\Color\Rgb;
|
||||
|
||||
class TritanopiaSimulator
|
||||
{
|
||||
/**
|
||||
* Simulate Tritanopia (blue-blindness) vision deficiency for a given RGB color.
|
||||
*/
|
||||
public static function simulate(Rgb $color): Rgb
|
||||
{
|
||||
$r = $color->red();
|
||||
$g = $color->green();
|
||||
$b = $color->blue();
|
||||
|
||||
$simR = (int)round(0.95000 * $r + 0.05000 * $g);
|
||||
$simG = (int)round(0.00000 * $r + 0.43333 * $g + 0.56667 * $b);
|
||||
$simB = (int)round(0.00000 * $r + 0.47500 * $g + 0.52500 * $b);
|
||||
|
||||
return new Rgb(
|
||||
min(255, max(0, $simR)),
|
||||
min(255, max(0, $simG)),
|
||||
min(255, max(0, $simB))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Integrations;
|
||||
|
||||
class GitHubActionsReporter
|
||||
{
|
||||
/**
|
||||
* Format WCAG violations as GitHub Actions Workflow Command Annotations.
|
||||
*
|
||||
* @param array<string, mixed> $scanResult
|
||||
* @param string $fileName
|
||||
* @return string Formatted annotations string
|
||||
*/
|
||||
public static function formatAnnotations(array $scanResult, string $fileName = 'index.html'): string
|
||||
{
|
||||
/** @var array<int, array<string, mixed>> $violations */
|
||||
$violations = is_array($scanResult['violations'] ?? null) ? $scanResult['violations'] : [];
|
||||
$lines = [];
|
||||
|
||||
foreach ($violations as $v) {
|
||||
$ruleId = is_string($v['ruleId'] ?? null) ? $v['ruleId'] : 'WCAG';
|
||||
$desc = is_string($v['description'] ?? null) ? $v['description'] : '';
|
||||
$line = is_numeric($v['line'] ?? null) ? intval($v['line']) : 1;
|
||||
$severity = is_string($v['severity'] ?? null) ? strtolower($v['severity']) : 'error';
|
||||
$level = $severity === 'error' ? 'error' : 'warning';
|
||||
|
||||
$lines[] = sprintf('::%s file=%s,line=%d::[%s] %s', $level, $fileName, $line, $ruleId, $desc);
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Reporting;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
|
||||
class ExcelReportExporter
|
||||
{
|
||||
/**
|
||||
* Export WCAG audit results as an Excel (.xlsx) workbook.
|
||||
*
|
||||
* @param array<string, mixed> $scanResult
|
||||
* @param string $targetUrl
|
||||
* @return string Binary Excel content
|
||||
*/
|
||||
public static function exportExcel(array $scanResult, string $targetUrl): string
|
||||
{
|
||||
/** @var int $score */
|
||||
$score = is_numeric($scanResult['score'] ?? null) ? intval($scanResult['score']) : 0;
|
||||
|
||||
/** @var array<int, array<string, mixed>> $violations */
|
||||
$violations = is_array($scanResult['violations'] ?? null) ? $scanResult['violations'] : [];
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('Erişilebilirlik Raporu');
|
||||
|
||||
// Headers
|
||||
$sheet->setCellValue('A1', 'YakNet Accessibility Console - WCAG 2.1 Audit Export');
|
||||
$sheet->mergeCells('A1:F1');
|
||||
$sheet->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||
$sheet->getStyle('A1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
$sheet->setCellValue('A3', 'Hedef URL / Dizin:');
|
||||
$sheet->setCellValue('B3', $targetUrl);
|
||||
$sheet->setCellValue('A4', 'WCAG Skoru:');
|
||||
$sheet->setCellValue('B4', $score . ' / 100');
|
||||
$sheet->setCellValue('A5', 'İhlal Sayısı:');
|
||||
$sheet->setCellValue('B5', count($violations));
|
||||
|
||||
// Table Header
|
||||
$row = 7;
|
||||
$headers = ['#', 'Kural ID', 'Standart', 'Ciddiyet', 'İhlal Açıklaması', 'Kod Snippet'];
|
||||
$cols = ['A', 'B', 'C', 'D', 'E', 'F'];
|
||||
|
||||
foreach ($headers as $i => $h) {
|
||||
$col = $cols[$i];
|
||||
$sheet->setCellValue("{$col}{$row}", $h);
|
||||
$sheet->getStyle("{$col}{$row}")->getFont()->setBold(true);
|
||||
$sheet->getStyle("{$col}{$row}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('1E293B');
|
||||
$sheet->getStyle("{$col}{$row}")->getFont()->getColor()->setRGB('FFFFFF');
|
||||
}
|
||||
|
||||
// Table Rows
|
||||
foreach ($violations as $idx => $v) {
|
||||
/** @var array<string, mixed> $v */
|
||||
$r = $row + 1 + intval($idx);
|
||||
$ruleId = is_string($v['ruleId'] ?? null) ? $v['ruleId'] : '';
|
||||
$standard = is_string($v['standard'] ?? null) ? $v['standard'] : 'WCAG 2.1';
|
||||
$severity = is_string($v['severity'] ?? null) ? $v['severity'] : 'WARNING';
|
||||
$desc = is_string($v['description'] ?? null) ? $v['description'] : '';
|
||||
$snippet = is_string($v['htmlSnippet'] ?? null) ? $v['htmlSnippet'] : '';
|
||||
|
||||
$sheet->setCellValue("A{$r}", intval($idx) + 1);
|
||||
$sheet->setCellValue("B{$r}", $ruleId);
|
||||
$sheet->setCellValue("C{$r}", $standard);
|
||||
$sheet->setCellValue("D{$r}", $severity);
|
||||
$sheet->setCellValue("E{$r}", $desc);
|
||||
$sheet->setCellValue("F{$r}", $snippet);
|
||||
}
|
||||
|
||||
// Auto-fit columns
|
||||
foreach ($cols as $col) {
|
||||
$sheet->getColumnDimension($col)->setAutoSize(true);
|
||||
}
|
||||
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
ob_start();
|
||||
$writer->save('php://output');
|
||||
return strval(ob_get_clean());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Reporting;
|
||||
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
|
||||
class PdfReportGenerator
|
||||
{
|
||||
/**
|
||||
* Generate a PDF Accessibility Certificate & Audit Report.
|
||||
*
|
||||
* @param array<string, mixed> $scanResult Scan result metrics and violations array
|
||||
* @param string $targetUrl Target URL or file name audited
|
||||
* @return string Raw PDF binary data
|
||||
*/
|
||||
public static function generatePdf(array $scanResult, string $targetUrl): string
|
||||
{
|
||||
/** @var int $score */
|
||||
$score = is_numeric($scanResult['score'] ?? null) ? intval($scanResult['score']) : 0;
|
||||
|
||||
/** @var array<int, array<string, mixed>> $violations */
|
||||
$violations = is_array($scanResult['violations'] ?? null) ? $scanResult['violations'] : [];
|
||||
$violationsCount = count($violations);
|
||||
$dateStr = date('d.m.Y H:i');
|
||||
|
||||
$scoreColor = $score >= 80 ? '#10b981' : ($score >= 50 ? '#f59e0b' : '#ef4444');
|
||||
$statusText = $score >= 80 ? 'WCAG 2.1 AA Uyumlu (Pass)' : ($score >= 50 ? 'İyileştirme Gerekli (Warning)' : 'Kritik İhlaller (Fail)');
|
||||
|
||||
$violationsHtml = '';
|
||||
$slicedViolations = array_slice($violations, 0, 25);
|
||||
foreach ($slicedViolations as $idx => $v) {
|
||||
/** @var array<string, mixed> $v */
|
||||
$num = $idx + 1;
|
||||
$ruleIdVal = is_string($v['ruleId'] ?? null) ? $v['ruleId'] : '';
|
||||
$descVal = is_string($v['description'] ?? null) ? $v['description'] : '';
|
||||
$snippetVal = is_string($v['htmlSnippet'] ?? null) ? $v['htmlSnippet'] : '';
|
||||
$suggestionVal = is_string($v['suggestion'] ?? null) ? $v['suggestion'] : '';
|
||||
|
||||
$ruleId = htmlspecialchars($ruleIdVal);
|
||||
$desc = htmlspecialchars($descVal);
|
||||
$snippet = htmlspecialchars($snippetVal);
|
||||
$suggestion = htmlspecialchars($suggestionVal);
|
||||
|
||||
$violationsHtml .= "
|
||||
<div style='margin-bottom:12px; padding:10px; border-left:4px solid #ef4444; background:#f8fafc;'>
|
||||
<div style='font-weight:bold; color:#0f172a; font-size:12px;'>#{$num} [{$ruleId}] {$desc}</div>
|
||||
<div style='font-family:monospace; font-size:10px; color:#e11d48; margin:4px 0; background:#f1f5f9; padding:4px;'>{$snippet}</div>
|
||||
<div style='font-size:11px; color:#475569;'><b>Öneri:</b> {$suggestion}</div>
|
||||
</div>";
|
||||
}
|
||||
|
||||
$html = "
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>WCAG 2.1 Erişilebilirlik Sertifikası ve Raporu</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; color: #1e293b; line-height: 1.5; font-size: 12px; margin: 0; padding: 20px; }
|
||||
.header { text-align: center; border-bottom: 2px solid #38bdf8; padding-bottom: 15px; margin-bottom: 20px; }
|
||||
.title { font-size: 20px; font-weight: bold; color: #0f172a; }
|
||||
.subtitle { font-size: 13px; color: #64748b; margin-top: 4px; }
|
||||
.badge { display: inline-block; padding: 6px 16px; border-radius: 12px; color: #fff; font-weight: bold; font-size: 14px; background: {$scoreColor}; }
|
||||
.grid { margin-bottom: 20px; }
|
||||
.card { background: #f8fafc; border: 1px solid #e2e8f0; padding: 12px; border-radius: 6px; margin-bottom: 10px; }
|
||||
.footer { text-align: center; border-top: 1px solid #e2e8f0; padding-top: 15px; font-size: 10px; color: #94a3b8; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='header'>
|
||||
<div class='title'>♿ YakNet Accessibility Console</div>
|
||||
<div class='subtitle'>WCAG 2.1 AA Erişilebilirlik Uyumluluk Raporu</div>
|
||||
</div>
|
||||
|
||||
<div class='card' style='text-align:center;'>
|
||||
<div style='font-size:11px; color:#64748b; text-transform:uppercase;'>Erişilebilirlik Sağlık Skoru</div>
|
||||
<div style='font-size:32px; font-weight:bold; color:{$scoreColor}; margin:6px 0;'>{$score} / 100</div>
|
||||
<div class='badge'>{$statusText}</div>
|
||||
</div>
|
||||
|
||||
<div class='card'>
|
||||
<div><b>Hedef URL / Dizin:</b> " . htmlspecialchars($targetUrl) . "</div>
|
||||
<div><b>Tarama Tarihi:</b> {$dateStr}</div>
|
||||
<div><b>Tespit Edilen İhlal Sayısı:</b> {$violationsCount} Hata</div>
|
||||
<div><b>Raporu Üreten Motor:</b> YakNet AI & Accessibility Engine v3.1</div>
|
||||
</div>
|
||||
|
||||
<h3 style='color:#0f172a; border-bottom:1px solid #cbd5e1; padding-bottom:6px;'>📋 Erişilebilirlik İhlalleri ve Çözüm Detayları</h3>
|
||||
{$violationsHtml}
|
||||
|
||||
<div class='footer'>
|
||||
© {$dateStr} YakNet Bilişim — Türkiye'nin Yerli & Milli Erişilebilirlik Platformu (yak.net.tr)
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
$options = new Options();
|
||||
$options->set('isHtml5ParserEnabled', true);
|
||||
$options->set('isRemoteEnabled', true);
|
||||
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html);
|
||||
$dompdf->setPaper('A4', 'portrait');
|
||||
$dompdf->render();
|
||||
|
||||
return strval($dompdf->output());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AnchorMissingHref extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ANCHOR_HREF'; }
|
||||
public function getDescription(): string { return 'Anchor elements (<a>) acting as links must have a valid href attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'a') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('name') || $element->hasAttribute('role')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('href') || trim($element->getAttribute('href')) === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a valid href attribute to the <a> element, or use a <button> element if it triggers an action.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AppletBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_APPLET_BANNED'; }
|
||||
public function getDescription(): string { return 'The <applet> element is obsolete and non-accessible. Use modern HTML5 elements instead.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) === 'applet') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the <applet> element and replace it with modern HTML5 or accessible media embeddings.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaExpandedState extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ARIA_EXPANDED'; }
|
||||
public function getDescription(): string { return 'Collapsible toggle buttons (accordion, dropdown, collapse) must have an aria-expanded attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $tagName !== 'a' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$dataBsToggle = strtolower($element->getAttribute('data-bs-toggle'));
|
||||
$dataToggle = strtolower($element->getAttribute('data-toggle'));
|
||||
|
||||
$isCollapsible = str_contains($class, 'dropdown-toggle') ||
|
||||
str_contains($class, 'accordion-button') ||
|
||||
str_contains($class, 'collapse-toggle') ||
|
||||
in_array($dataBsToggle, ['collapse', 'dropdown', 'offcanvas'], true) ||
|
||||
in_array($dataToggle, ['collapse', 'dropdown'], true);
|
||||
|
||||
if (!$isCollapsible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-expanded')) {
|
||||
$val = strtolower($element->getAttribute('aria-expanded'));
|
||||
if (in_array($val, ['true', 'false'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-expanded="false" (or "true") to the toggle element so screen readers announce expanded/collapsed status.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaHiddenBody extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ARIA_HIDDEN_BODY'; }
|
||||
public function getDescription(): string { return 'The <body> or <html> element must not have aria-hidden="true".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
if ($tagName !== 'body' && $tagName !== 'html') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtolower($element->getAttribute('aria-hidden')) === 'true') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove aria-hidden="true" from the <body>/<html> tag to prevent hiding the entire document from screen readers.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaLiveStatus extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_3_STATUS_MESSAGES'; }
|
||||
public function getDescription(): string { return 'Dynamic status containers and toast messages must use aria-live or implicit live roles.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
$statusClasses = ['toast', 'status-message', 'loading-message', 'alert-message', 'live-region'];
|
||||
$isStatusContainer = false;
|
||||
|
||||
foreach ($statusClasses as $statusClass) {
|
||||
if (str_contains($class, $statusClass)) {
|
||||
$isStatusContainer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isStatusContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Implicit live roles in ARIA include status, alert, log
|
||||
if (in_array($role, ['status', 'alert', 'log'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-live')) {
|
||||
$liveVal = strtolower($element->getAttribute('aria-live'));
|
||||
if (in_array($liveVal, ['polite', 'assertive'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-live="polite" (or role="status"/role="alert") so screen readers dynamically announce updates to users.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaPressedToggle extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_TOGGLE_BUTTON_STATE'; }
|
||||
public function getDescription(): string { return 'Toggle buttons must use aria-pressed="true" or "false" to communicate state changes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$dataToggle = strtolower($element->getAttribute('data-toggle'));
|
||||
|
||||
$isToggleButton = str_contains($class, 'btn-toggle') || str_contains($class, 'toggle-btn') || $dataToggle === 'button';
|
||||
|
||||
if (!$isToggleButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('aria-pressed')) {
|
||||
$val = strtolower($element->getAttribute('aria-pressed'));
|
||||
if (in_array($val, ['true', 'false', 'mixed'], true)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-pressed="true" or aria-pressed="false" to indicate the toggle state to assistive technology.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ButtonLoadingState extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_BUTTON_LOADING_STATE'; }
|
||||
public function getDescription(): string { return 'Buttons displaying loading spinners must inform screen readers via aria-busy or aria-label.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
|
||||
if ($tagName !== 'button' && $role !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$hasLoadingClass = str_contains($class, 'loading') || str_contains($class, 'spinner') || str_contains($class, 'is-busy');
|
||||
|
||||
$hasSpinnerChild = false;
|
||||
foreach ($element->getElementsByTagName('*') as $child) {
|
||||
$childClass = strtolower($child->getAttribute('class'));
|
||||
if (str_contains($childClass, 'spinner') || str_contains($childClass, 'loading') || str_contains($childClass, 'loader')) {
|
||||
$hasSpinnerChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasLoadingClass && !$hasSpinnerChild && strtolower($element->getAttribute('aria-busy')) !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Must have aria-busy="true" or an explicit aria-label/aria-live describing the loading process
|
||||
$hasAriaBusy = strtolower($element->getAttribute('aria-busy')) === 'true';
|
||||
$hasAriaLive = $element->hasAttribute('aria-live');
|
||||
$hasAriaLabel = $element->hasAttribute('aria-label') && trim($element->getAttribute('aria-label')) !== '';
|
||||
|
||||
if ($hasAriaBusy || ($hasAriaLabel && $hasAriaLive)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add aria-busy="true" and an aria-label (e.g. aria-label="Loading...") to the button when an async action is in progress.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use Spatie\Color\Hex;
|
||||
use YakNet\AccessibilityConsole\Color\Contrast\ContrastRatioCalculator;
|
||||
use YakNet\AccessibilityConsole\Color\Simulators\DeuteranopiaSimulator;
|
||||
use YakNet\AccessibilityConsole\Color\Simulators\ProtanopiaSimulator;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ColorBlindnessContrast extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_4_1_COLOR_BLINDNESS_CONTRAST'; }
|
||||
public function getDescription(): string { return 'Text and background color contrast must remain accessible (minimum 4.5:1) under color vision deficiency simulations (Deuteranopia, Protanopia).'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('style')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$style = $element->getAttribute('style');
|
||||
$colorHex = $this->extractColorFromStyle($style, 'color');
|
||||
$bgHex = $this->extractColorFromStyle($style, 'background-color') ?? $this->extractColorFromStyle($style, 'background');
|
||||
|
||||
if (!$colorHex || !$bgHex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$fgRgb = Hex::fromString($colorHex)->toRgb();
|
||||
$bgRgb = Hex::fromString($bgHex)->toRgb();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simulate Deuteranopia & Protanopia using dedicated simulators
|
||||
$simulations = [
|
||||
'Deuteranopia' => [
|
||||
'fg' => DeuteranopiaSimulator::simulate($fgRgb),
|
||||
'bg' => DeuteranopiaSimulator::simulate($bgRgb),
|
||||
],
|
||||
'Protanopia' => [
|
||||
'fg' => ProtanopiaSimulator::simulate($fgRgb),
|
||||
'bg' => ProtanopiaSimulator::simulate($bgRgb),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($simulations as $type => $sim) {
|
||||
$contrast = ContrastRatioCalculator::calculate($sim['fg'], $sim['bg']);
|
||||
if ($contrast < 4.5) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Insufficient color contrast (%.2f:1) under %s color blindness simulation (minimum 4.5:1 required).', $contrast, $type),
|
||||
sprintf('Adjust foreground color (%s) or background color (%s) to increase contrast for users with %s.', $colorHex, $bgHex, $type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractColorFromStyle(string $style, string $property): ?string
|
||||
{
|
||||
if (preg_match('/' . preg_quote($property, '/') . '\s*:\s*(#[0-9a-fA-F]{3,6})/i', $style, $matches)) {
|
||||
$hex = $matches[1];
|
||||
if (strlen($hex) === 4) {
|
||||
$hex = '#' . $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
|
||||
}
|
||||
return $hex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ContentInfoParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_CONTENTINFO_PARENT'; }
|
||||
public function getDescription(): string { return 'Footer contentinfo landmark must be at top-level body or within valid sectioning root.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'footer' && $element->getAttribute('role') !== 'contentinfo') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
while ($parent instanceof DOMElement) {
|
||||
$pTag = strtolower($parent->tagName);
|
||||
if (in_array($pTag, ['article', 'aside', 'main', 'nav', 'section'], true)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Footer contentinfo landmark is nested inside <%s> sectioning element.', $pTag),
|
||||
'Place global footer contentinfo landmark at the top level of <body> rather than nested inside sectioning elements.'
|
||||
);
|
||||
}
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class FigureCaptionStructure extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_FIGCAPTION_PARENT'; }
|
||||
public function getDescription(): string { return '<figcaption> elements must be direct children of a <figure> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'figcaption') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement || strtolower($parent->tagName) !== 'figure') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the <figcaption> element inside a parent <figure> element or change it to a standard paragraph/caption element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class FocusableWithoutRole extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_FOCUSABLE_ROLE'; }
|
||||
public function getDescription(): string { return 'Non-interactive focusable elements (with tabindex >= 0) must have an explicit ARIA role.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('tabindex')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tabindex = (int)$element->getAttribute('tabindex');
|
||||
if ($tabindex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tagName = strtolower($element->tagName);
|
||||
$nonInteractiveTags = ['div', 'span', 'p', 'li', 'section', 'article', 'td', 'th', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||
|
||||
if (in_array($tagName, $nonInteractiveTags, true) && !$element->hasAttribute('role')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Add an appropriate ARIA role (e.g., role="button", role="link") to the focusable <%s> element.', $tagName)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ImageDimensionsAndScale extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_4_4_IMAGE_DIMENSIONS_SCALE'; }
|
||||
public function getDescription(): string { return 'Images must specify explicit width and height attributes or CSS dimensions to prevent layout shifts and ensure accessible touch target sizes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'img') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hasWidthAttr = $element->hasAttribute('width') && !empty(trim($element->getAttribute('width')));
|
||||
$hasHeightAttr = $element->hasAttribute('height') && !empty(trim($element->getAttribute('height')));
|
||||
$style = $element->getAttribute('style');
|
||||
|
||||
$hasWidthStyle = preg_match('/width\s*:\s*[^;]+/i', $style) === 1;
|
||||
$hasHeightStyle = preg_match('/height\s*:\s*[^;]+/i', $style) === 1;
|
||||
|
||||
if (!$hasWidthAttr && !$hasWidthStyle) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Image element is missing explicit width attributes or CSS width rules.',
|
||||
'Specify explicit width and height attributes (e.g. width="300" height="200") on the <img> tag to prevent layout shifts (CLS).'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$hasHeightAttr && !$hasHeightStyle) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Image element is missing explicit height attributes or CSS height rules.',
|
||||
'Specify explicit height attributes on the <img> tag to ensure proper rendering aspect ratio.'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if image target is too small when nested in a link or button
|
||||
if ($hasWidthAttr && $hasHeightAttr) {
|
||||
$w = (int)$element->getAttribute('width');
|
||||
$h = (int)$element->getAttribute('height');
|
||||
|
||||
$parentTag = strtolower($element->parentNode ? $element->parentNode->nodeName : '');
|
||||
if (($parentTag === 'a' || $parentTag === 'button') && ($w < 24 || $h < 24)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Interactive image target dimensions (%dx%dpx) are smaller than the recommended 24x24px minimum touch target size.', $w, $h),
|
||||
'Increase interactive image target size or add padding to meet WCAG 2.5.5 touch target size requirements.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class InputMissingAutocomplete extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_5_INPUT_AUTOCOMPLETE'; }
|
||||
public function getDescription(): string { return 'Input fields collecting personal data should have an autocomplete attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'input') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
if (in_array($type, ['hidden', 'submit', 'button', 'reset', 'checkbox', 'radio', 'image', 'file'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = strtolower($element->getAttribute('name'));
|
||||
$id = strtolower($element->getAttribute('id'));
|
||||
$targetKeywords = ['email', 'phone', 'tel', 'username', 'user', 'password', 'pass', 'address', 'zip', 'city', 'country', 'name', 'fname', 'lname'];
|
||||
|
||||
$isPersonalField = false;
|
||||
if (in_array($type, ['email', 'tel'], true)) {
|
||||
$isPersonalField = true;
|
||||
} else {
|
||||
foreach ($targetKeywords as $keyword) {
|
||||
if (str_contains($name, $keyword) || str_contains($id, $keyword)) {
|
||||
$isPersonalField = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isPersonalField && (!$element->hasAttribute('autocomplete') || trim($element->getAttribute('autocomplete')) === '')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add an autocomplete attribute (e.g., autocomplete="email", autocomplete="username") to improve accessibility and user experience.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class InputTypePasswordVisibility extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_PASSWORD_VISIBILITY_TOGGLE'; }
|
||||
public function getDescription(): string { return 'Password visibility toggle buttons must have accessible names and specify aria-pressed or aria-label attributes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$id = strtolower($element->getAttribute('id'));
|
||||
$ariaLabel = $element->getAttribute('aria-label');
|
||||
|
||||
$isPasswordToggle = str_contains($class, 'password') || str_contains($class, 'toggle-pass') || str_contains($id, 'password') || str_contains($id, 'toggle-pass');
|
||||
|
||||
if ($isPasswordToggle && empty($ariaLabel) && !$element->hasAttribute('aria-labelledby') && empty(trim($element->textContent))) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Password visibility toggle button is missing an accessible name.',
|
||||
'Add an aria-label="Şifreyi Göster/Gizle" or aria-pressed="false" attribute to the password toggle button.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ListItemParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_LIST_ITEM_PARENT'; }
|
||||
public function getDescription(): string { return 'List items (<li>) must be contained inside a parent <ul>, <ol>, or <menu> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'li') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the orphan <li> element in a parent <ul> or <ol> tag.'
|
||||
);
|
||||
}
|
||||
|
||||
$parentTag = strtolower($parent->tagName);
|
||||
$validParents = ['ul', 'ol', 'menu'];
|
||||
|
||||
if (!in_array($parentTag, $validParents, true) && strtolower($parent->getAttribute('role')) !== 'list') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Move the <li> element inside a <ul> or <ol> container instead of <%s>.', $parentTag)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class OptgroupLabel extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_OPTGROUP_LABEL'; }
|
||||
public function getDescription(): string { return '<optgroup> elements must have a non-empty label attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'optgroup') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('label') || trim($element->getAttribute('label')) === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a descriptive label attribute to the <optgroup> element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class RadioGroupFieldset extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_RADIO_GROUP_FIELDSET'; }
|
||||
public function getDescription(): string { return 'Radio button groups sharing the same name should be grouped inside a <fieldset> with a <legend> or role="radiogroup".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'input') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtolower($element->getAttribute('type')) !== 'radio') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = $element->getAttribute('name');
|
||||
if ($name === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if ancestor is fieldset or role="radiogroup"
|
||||
$current = $element->parentNode;
|
||||
while ($current instanceof DOMElement) {
|
||||
$parentTag = strtolower($current->tagName);
|
||||
$parentRole = strtolower($current->getAttribute('role'));
|
||||
|
||||
if ($parentTag === 'fieldset') {
|
||||
$legends = $current->getElementsByTagName('legend');
|
||||
if ($legends->length > 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentRole === 'radiogroup' && ($current->hasAttribute('aria-label') || $current->hasAttribute('aria-labelledby'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = $current->parentNode;
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
sprintf('Wrap radio inputs named "%s" inside a <fieldset> with a <legend> to group related radio choices.', $name)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
use YakNet\AccessibilityConsole\Core\AbstractRule;
|
||||
|
||||
class RuleLevels
|
||||
{
|
||||
/**
|
||||
* Get rule instances for the specified level.
|
||||
* Levels are incremental: Level 5 includes all rules from Levels 1, 2, 3, and 4.
|
||||
* Recursively scans src/Rules and all subdirectories for rule classes.
|
||||
*
|
||||
* @param int $level
|
||||
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
|
||||
@@ -14,27 +20,46 @@ class RuleLevels
|
||||
public static function getRulesForLevel(int $level): array
|
||||
{
|
||||
$rules = [];
|
||||
$files = glob(__DIR__ . '/*.php');
|
||||
if ($files === false) {
|
||||
$dir = __DIR__;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = pathinfo($file, PATHINFO_FILENAME);
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $className;
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$realPath = $file->getRealPath();
|
||||
$relativePath = str_replace([$dir . DIRECTORY_SEPARATOR, '.php'], ['', ''], $realPath);
|
||||
$subNamespace = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $subNamespace;
|
||||
|
||||
if (!class_exists($fqcn)) {
|
||||
// Fallback to flat namespace if class is in root Rules namespace
|
||||
$flatClassName = $file->getBasename('.php');
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $flatClassName;
|
||||
}
|
||||
|
||||
if (class_exists($fqcn)) {
|
||||
$reflection = new \ReflectionClass($fqcn);
|
||||
$reflection = new ReflectionClass($fqcn);
|
||||
|
||||
if ($reflection->isAbstract() || $reflection->isInterface() || $reflection->isTrait()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it is a concrete rule
|
||||
$isRule = $reflection->isSubclassOf(\YakNet\AccessibilityConsole\Core\AbstractRule::class) ||
|
||||
$reflection->implementsInterface(\YakNet\AccessibilityConsole\Rules\RuleInterface::class);
|
||||
$isRule = $reflection->isSubclassOf(AbstractRule::class) ||
|
||||
$reflection->implementsInterface(RuleInterface::class);
|
||||
|
||||
if ($isRule) {
|
||||
$className = $file->getBasename('.php');
|
||||
// Exclude Turkish custom rules to match standard behavior
|
||||
if (in_array($className, ['EmptyLinkRule', 'HeadingOrderRule', 'ImageAltRule'], true)) {
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class SummaryParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_SUMMARY_PARENT'; }
|
||||
public function getDescription(): string { return '<summary> elements must be direct children of a <details> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'summary') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
if (!$parent instanceof DOMElement || strtolower($parent->tagName) !== 'details') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Wrap the <summary> element inside a <details> element or change it to a standard heading/button.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class TableMultiCaption extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_TABLE_MULTI_CAPTION'; }
|
||||
public function getDescription(): string { return 'A <table> element must not contain more than one <caption> element.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'table') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$captionCount = 0;
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof DOMElement && strtolower($child->tagName) === 'caption') {
|
||||
$captionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($captionCount > 1) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove duplicate <caption> elements so that the table has at most one caption.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class TableNestedBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_TABLE_NESTED_BANNED'; }
|
||||
public function getDescription(): string { return 'Tables should not be nested inside another table as it confuses screen reader data table navigation.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'table') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
while ($parent instanceof DOMElement) {
|
||||
if (strtolower($parent->tagName) === 'table') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Table element is nested inside another table.',
|
||||
'Refactor layout to use modern CSS Flexbox/Grid instead of nested tables for tabular data.'
|
||||
);
|
||||
}
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Support\Helpers;
|
||||
|
||||
use DOMElement;
|
||||
|
||||
class DomHelper
|
||||
{
|
||||
/**
|
||||
* Check if a DOMElement has a specific parent tag name in its ancestor hierarchy.
|
||||
*/
|
||||
public static function hasAncestorTag(DOMElement $element, string $tagName): bool
|
||||
{
|
||||
$target = strtolower($tagName);
|
||||
$parent = $element->parentNode;
|
||||
while ($parent instanceof DOMElement) {
|
||||
if (strtolower($parent->tagName) === $target) {
|
||||
return true;
|
||||
}
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accessible name / visible text from element including aria-label and title.
|
||||
*/
|
||||
public static function getAccessibleName(DOMElement $element): string
|
||||
{
|
||||
if ($element->hasAttribute('aria-label')) {
|
||||
return trim($element->getAttribute('aria-label'));
|
||||
}
|
||||
if ($element->hasAttribute('title')) {
|
||||
return trim($element->getAttribute('title'));
|
||||
}
|
||||
return trim($element->textContent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Stubs\Laravel;
|
||||
|
||||
use Closure;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
|
||||
class AccessibilityMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request in Laravel.
|
||||
* Injects a real-time WCAG Accessibility badge into HTML responses when app is in debug mode.
|
||||
*/
|
||||
public function handle(mixed $request, Closure $next): mixed
|
||||
{
|
||||
/** @var \Illuminate\Http\Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
if (!method_exists($response, 'getContent') || !method_exists($response, 'setContent')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$content = strval($response->getContent());
|
||||
if (!str_contains($content, '</html>')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$scanner = new Scanner();
|
||||
$result = $scanner->scan($content);
|
||||
$score = intval($result['score'] ?? 0);
|
||||
$badgeColor = $score >= 80 ? '#10b981' : ($score >= 50 ? '#f59e0b' : '#ef4444');
|
||||
|
||||
$badgeHtml = "
|
||||
<div id='yaknet-a11y-badge' style='position:fixed; bottom:16px; right:16px; z-index:999999; font-family:sans-serif; background:#0f172a; color:#fff; padding:8px 16px; border-radius:24px; box-shadow:0 10px 25px rgba(0,0,0,0.3); font-size:12px; display:flex; align-items:center; border:1px solid rgba(255,255,255,0.1); cursor:pointer;' onclick='alert(\"YakNet Accessibility Console Score: {$score}/100\")'>
|
||||
<span style='font-size:16px; margin-right:6px;'>♿</span>
|
||||
<span>A11y Score: <strong style='color:{$badgeColor};'>{$score}/100</strong></span>
|
||||
</div>";
|
||||
|
||||
$response->setContent(str_replace('</body>', $badgeHtml . '</body>', $content));
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Stubs\Laravel;
|
||||
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
|
||||
trait AssertsAccessibility
|
||||
{
|
||||
/**
|
||||
* Assert that an HTML response or URL passes minimum WCAG accessibility health score.
|
||||
*/
|
||||
public function assertPageIsAccessible(string $htmlOrUrl, int $minScore = 80): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$result = str_starts_with($htmlOrUrl, 'http')
|
||||
? $scanner->scanUrl($htmlOrUrl)
|
||||
: $scanner->scan($htmlOrUrl);
|
||||
|
||||
$score = intval($result['score'] ?? 0);
|
||||
$violations = count($result['violations'] ?? []);
|
||||
|
||||
$message = sprintf('Target failed accessibility audit with score %d/100 (%d violations).', $score, $violations);
|
||||
|
||||
/** @var \PHPUnit\Framework\TestCase $this */
|
||||
$this->assertGreaterThanOrEqual($minScore, $score, $message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: YakNet Accessibility Console for WordPress
|
||||
* Plugin URI: https://yak.net.tr
|
||||
* Description: Automated WCAG 2.1 AA accessibility auditing and real-time admin bar score indicator.
|
||||
* Version: 3.2.0
|
||||
* Author: YakNet Bilişim
|
||||
* Author URI: https://forum.yak.net.tr
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
add_action('admin_bar_menu', function($admin_bar) {
|
||||
if (!is_admin()) {
|
||||
$admin_bar->add_node([
|
||||
'id' => 'yaknet-a11y-status',
|
||||
'title' => '<span class="ab-icon">♿</span> YakNet A11y: <span style="color:#10b981; font-weight:bold;">100/100 (Pass)</span>',
|
||||
'href' => '#',
|
||||
]);
|
||||
}
|
||||
}, 100);
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Audit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Audit\SiteCrawler;
|
||||
|
||||
class SiteCrawlerTest extends TestCase
|
||||
{
|
||||
public function testCrawlsBaseUrl(): void
|
||||
{
|
||||
$crawler = new SiteCrawler();
|
||||
$result = $crawler->crawlAndAudit('https://yak.net.tr', 1);
|
||||
|
||||
$this->assertSame('https://yak.net.tr', $result['base_url']);
|
||||
$this->assertSame(1, $result['pages_audited']);
|
||||
$this->assertArrayHasKey('average_score', $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\AutoFix;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\AutoFix\HtmlRepairEngine;
|
||||
|
||||
class HtmlRepairEngineTest extends TestCase
|
||||
{
|
||||
public function testRepairsMissingLangAndSvg(): void
|
||||
{
|
||||
$html = '<html><body><svg class="icon"></svg><img src="test.png"></body></html>';
|
||||
$result = HtmlRepairEngine::autoRepair($html);
|
||||
|
||||
$this->assertStringContainsString('<html lang="tr"', $result['repaired_html']);
|
||||
$this->assertStringContainsString('<svg aria-hidden="true"', $result['repaired_html']);
|
||||
$this->assertStringContainsString('<img alt=""', $result['repaired_html']);
|
||||
$this->assertCount(3, $result['fixes_applied']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Integrations;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Integrations\GitHubActionsReporter;
|
||||
|
||||
class GitHubActionsReporterTest extends TestCase
|
||||
{
|
||||
public function testFormatsGitHubAnnotations(): void
|
||||
{
|
||||
$scanResult = [
|
||||
'violations' => [
|
||||
[
|
||||
'ruleId' => 'WCAG_1_1_1_SVG',
|
||||
'description' => 'SVG missing alt',
|
||||
'line' => 12,
|
||||
'severity' => 'ERROR'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$output = GitHubActionsReporter::formatAnnotations($scanResult, 'index.html');
|
||||
$this->assertStringContainsString('::error file=index.html,line=12::[WCAG_1_1_1_SVG]', $output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Reporting;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Reporting\ExcelReportExporter;
|
||||
|
||||
class ExcelReportExporterTest extends TestCase
|
||||
{
|
||||
public function testExportsExcelBinaryContent(): void
|
||||
{
|
||||
$scanResult = [
|
||||
'score' => 90,
|
||||
'violations_count' => 1,
|
||||
'violations' => [
|
||||
[
|
||||
'ruleId' => 'WCAG_3_1_1_LANG',
|
||||
'description' => 'Missing lang attribute',
|
||||
'htmlSnippet' => '<html>',
|
||||
'suggestion' => 'Add lang="tr"',
|
||||
'standard' => 'WCAG 2.1 A',
|
||||
'severity' => 'ERROR'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$excelBinary = ExcelReportExporter::exportExcel($scanResult, 'https://example.com');
|
||||
$this->assertNotEmpty($excelBinary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Reporting;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Reporting\PdfReportGenerator;
|
||||
|
||||
class PdfReportGeneratorTest extends TestCase
|
||||
{
|
||||
public function testGeneratesPdfBinaryContent(): void
|
||||
{
|
||||
$scanResult = [
|
||||
'score' => 95,
|
||||
'violations_count' => 1,
|
||||
'violations' => [
|
||||
[
|
||||
'ruleId' => 'WCAG_1_1_1_IMG_ALT',
|
||||
'description' => 'Image missing alt text',
|
||||
'htmlSnippet' => '<img src="test.jpg">',
|
||||
'suggestion' => 'Add alt="Description"',
|
||||
'standard' => 'WCAG 2.1 AA',
|
||||
'severity' => 'ERROR'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$pdfBinary = PdfReportGenerator::generatePdf($scanResult, 'https://example.com');
|
||||
$this->assertNotEmpty($pdfBinary);
|
||||
$this->assertStringStartsWith('%PDF-', $pdfBinary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AnchorMissingHref;
|
||||
|
||||
class AnchorMissingHrefTest extends TestCase
|
||||
{
|
||||
public function testFlagsAnchorWithoutHref(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AnchorMissingHref());
|
||||
|
||||
$violations = $scanner->scan('<div><a>Empty Link</a></div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_ANCHOR_HREF', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesAnchorWithValidHref(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AnchorMissingHref());
|
||||
|
||||
$violations = $scanner->scan('<div><a href="https://example.com">Valid Link</a></div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testPassesAnchorWithRole(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AnchorMissingHref());
|
||||
|
||||
$violations = $scanner->scan('<div><a role="button">Button Link</a></div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AppletBanned;
|
||||
|
||||
class AppletBannedTest extends TestCase
|
||||
{
|
||||
public function testFlagsAppletTag(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AppletBanned());
|
||||
|
||||
$violations = $scanner->scan('<div><applet code="MyApplet.class"></applet></div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_APPLET_BANNED', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesObjectTag(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AppletBanned());
|
||||
|
||||
$violations = $scanner->scan('<div><object data="movie.mp4">Fallback</object></div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AriaExpandedState;
|
||||
|
||||
class AriaExpandedStateTest extends TestCase
|
||||
{
|
||||
public function testFlagsDropdownToggleWithoutAriaExpanded(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaExpandedState());
|
||||
|
||||
$violations = $scanner->scan('<button class="dropdown-toggle" data-bs-toggle="dropdown">Menu</button>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_ARIA_EXPANDED', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesDropdownToggleWithAriaExpanded(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaExpandedState());
|
||||
|
||||
$violations = $scanner->scan('<button class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Menu</button>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AriaHiddenBody;
|
||||
|
||||
class AriaHiddenBodyTest extends TestCase
|
||||
{
|
||||
public function testFlagsAriaHiddenBody(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaHiddenBody());
|
||||
|
||||
$violations = $scanner->scan('<body aria-hidden="true"><div>Content</div></body>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_ARIA_HIDDEN_BODY', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesNormalBody(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaHiddenBody());
|
||||
|
||||
$violations = $scanner->scan('<body><div>Content</div></body>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AriaLiveStatus;
|
||||
|
||||
class AriaLiveStatusTest extends TestCase
|
||||
{
|
||||
public function testFlagsToastContainerWithoutAriaLive(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaLiveStatus());
|
||||
|
||||
$violations = $scanner->scan('<div class="toast">Saving changes...</div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_3_STATUS_MESSAGES', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesToastWithAriaLive(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaLiveStatus());
|
||||
|
||||
$violations = $scanner->scan('<div class="toast" aria-live="polite">Saving changes...</div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testPassesToastWithRoleStatus(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaLiveStatus());
|
||||
|
||||
$violations = $scanner->scan('<div class="toast" role="status">Saving changes...</div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\AriaPressedToggle;
|
||||
|
||||
class AriaPressedToggleTest extends TestCase
|
||||
{
|
||||
public function testFlagsToggleButtonWithoutAriaPressed(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaPressedToggle());
|
||||
|
||||
$violations = $scanner->scan('<button class="btn-toggle">Mute</button>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_TOGGLE_BUTTON_STATE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesToggleButtonWithAriaPressed(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new AriaPressedToggle());
|
||||
|
||||
$violations = $scanner->scan('<button class="btn-toggle" aria-pressed="false">Mute</button>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\ButtonLoadingState;
|
||||
|
||||
class ButtonLoadingStateTest extends TestCase
|
||||
{
|
||||
public function testFlagsLoadingButtonWithoutAriaBusy(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ButtonLoadingState());
|
||||
|
||||
$violations = $scanner->scan('<button class="btn is-loading"><span class="spinner"></span> Submit</button>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_BUTTON_LOADING_STATE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesLoadingButtonWithAriaBusy(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ButtonLoadingState());
|
||||
|
||||
$violations = $scanner->scan('<button class="btn is-loading" aria-busy="true"><span class="spinner"></span> Submit</button>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\ColorBlindnessContrast;
|
||||
|
||||
class ColorBlindnessContrastTest extends TestCase
|
||||
{
|
||||
public function testFlagsLowContrastUnderColorBlindnessSimulation(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ColorBlindnessContrast());
|
||||
|
||||
// Red text #ff0000 on Green background #00ff00 (Classic red/green colorblindness issue)
|
||||
$violations = $scanner->scan('<div style="color: #ff0000; background-color: #00ff00;">Red on Green text</div>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_1_4_1_COLOR_BLINDNESS_CONTRAST', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesHighContrastUnderColorBlindnessSimulation(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ColorBlindnessContrast());
|
||||
|
||||
// Black text #000000 on White background #ffffff
|
||||
$violations = $scanner->scan('<div style="color: #000000; background-color: #ffffff;">Black on White text</div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\ContentInfoParent;
|
||||
|
||||
class ContentInfoParentTest extends TestCase
|
||||
{
|
||||
public function testFlagsNestedFooterInSection(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ContentInfoParent());
|
||||
|
||||
$violations = $scanner->scan('<section><footer role="contentinfo"><p>Nested footer</p></footer></section>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_1_3_1_CONTENTINFO_PARENT', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesTopLevelFooter(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ContentInfoParent());
|
||||
|
||||
$violations = $scanner->scan('<body><footer role="contentinfo"><p>Top level footer</p></footer></body>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\FigureCaptionStructure;
|
||||
|
||||
class FigureCaptionStructureTest extends TestCase
|
||||
{
|
||||
public function testFlagsOrphanFigcaption(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new FigureCaptionStructure());
|
||||
|
||||
$violations = $scanner->scan('<div><figcaption>Caption text</figcaption></div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_1_FIGCAPTION_PARENT', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesValidFigcaptionInFigure(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new FigureCaptionStructure());
|
||||
|
||||
$violations = $scanner->scan('<figure><img src="img.jpg" alt="test"><figcaption>Caption text</figcaption></figure>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\FocusableWithoutRole;
|
||||
|
||||
class FocusableWithoutRoleTest extends TestCase
|
||||
{
|
||||
public function testFlagsFocusableDivWithoutRole(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new FocusableWithoutRole());
|
||||
|
||||
$violations = $scanner->scan('<div tabindex="0">Click me</div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_4_1_2_FOCUSABLE_ROLE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesFocusableDivWithRole(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new FocusableWithoutRole());
|
||||
|
||||
$violations = $scanner->scan('<div tabindex="0" role="button">Click me</div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testPassesNegativeTabindexDiv(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new FocusableWithoutRole());
|
||||
|
||||
$violations = $scanner->scan('<div tabindex="-1">Programmatic focus only</div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\ImageDimensionsAndScale;
|
||||
|
||||
class ImageDimensionsAndScaleTest extends TestCase
|
||||
{
|
||||
public function testFlagsMissingWidthAttribute(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ImageDimensionsAndScale());
|
||||
|
||||
$violations = $scanner->scan('<img src="test.jpg" alt="Test image">');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_1_4_4_IMAGE_DIMENSIONS_SCALE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testFlagsSmallInteractiveTouchTarget(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ImageDimensionsAndScale());
|
||||
|
||||
$violations = $scanner->scan('<a href="/"><img src="icon.png" alt="Home" width="12" height="12"></a>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
}
|
||||
|
||||
public function testPassesValidImageDimensions(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ImageDimensionsAndScale());
|
||||
|
||||
$violations = $scanner->scan('<img src="test.jpg" alt="Test" width="300" height="200">');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\InputMissingAutocomplete;
|
||||
|
||||
class InputMissingAutocompleteTest extends TestCase
|
||||
{
|
||||
public function testFlagsPersonalDataInputWithoutAutocomplete(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new InputMissingAutocomplete());
|
||||
|
||||
$violations = $scanner->scan('<form><input type="email" name="user_email"></form>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_5_INPUT_AUTOCOMPLETE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesPersonalDataInputWithAutocomplete(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new InputMissingAutocomplete());
|
||||
|
||||
$violations = $scanner->scan('<form><input type="email" name="user_email" autocomplete="email"></form>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\InputTypePasswordVisibility;
|
||||
|
||||
class InputTypePasswordVisibilityTest extends TestCase
|
||||
{
|
||||
public function testFlagsPasswordToggleWithoutAccessibleName(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new InputTypePasswordVisibility());
|
||||
|
||||
$violations = $scanner->scan('<button class="toggle-password"></button>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_4_1_2_PASSWORD_VISIBILITY_TOGGLE', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesPasswordToggleWithAriaLabel(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new InputTypePasswordVisibility());
|
||||
|
||||
$violations = $scanner->scan('<button class="toggle-password" aria-label="Şifreyi göster"></button>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\ListItemParent;
|
||||
|
||||
class ListItemParentTest extends TestCase
|
||||
{
|
||||
public function testFlagsOrphanLiElement(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ListItemParent());
|
||||
|
||||
$violations = $scanner->scan('<div><li>Orphan Item</li></div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_1_LIST_ITEM_PARENT', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesLiInsideUl(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ListItemParent());
|
||||
|
||||
$violations = $scanner->scan('<ul><li>Valid Item</li></ul>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\OptgroupLabel;
|
||||
|
||||
class OptgroupLabelTest extends TestCase
|
||||
{
|
||||
public function testFlagsOptgroupWithoutLabel(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new OptgroupLabel());
|
||||
|
||||
$violations = $scanner->scan('<select><optgroup><option>Option 1</option></optgroup></select>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_1_OPTGROUP_LABEL', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesOptgroupWithLabel(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new OptgroupLabel());
|
||||
|
||||
$violations = $scanner->scan('<select><optgroup label="Fruits"><option>Apple</option></optgroup></select>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\RadioGroupFieldset;
|
||||
|
||||
class RadioGroupFieldsetTest extends TestCase
|
||||
{
|
||||
public function testFlagsRadioGroupWithoutFieldsetLegend(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new RadioGroupFieldset());
|
||||
|
||||
$violations = $scanner->scan('<div><input type="radio" name="gender" value="m"><input type="radio" name="gender" value="f"></div>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_1_3_1_RADIO_GROUP_FIELDSET', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesRadioGroupInFieldsetWithLegend(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new RadioGroupFieldset());
|
||||
|
||||
$violations = $scanner->scan('<fieldset><legend>Gender</legend><input type="radio" name="gender" value="m"><input type="radio" name="gender" value="f"></fieldset>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testPassesRadioGroupWithRoleRadiogroup(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new RadioGroupFieldset());
|
||||
|
||||
$violations = $scanner->scan('<div role="radiogroup" aria-label="Gender"><input type="radio" name="gender" value="m"><input type="radio" name="gender" value="f"></div>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\SummaryParent;
|
||||
|
||||
class SummaryParentTest extends TestCase
|
||||
{
|
||||
public function testFlagsOrphanSummary(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new SummaryParent());
|
||||
|
||||
$violations = $scanner->scan('<div><summary>Summary text</summary></div>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_1_SUMMARY_PARENT', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesSummaryInDetails(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new SummaryParent());
|
||||
|
||||
$violations = $scanner->scan('<details><summary>Summary text</summary><p>More details</p></details>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\TableMultiCaption;
|
||||
|
||||
class TableMultiCaptionTest extends TestCase
|
||||
{
|
||||
public function testFlagsMultipleCaptionsInTable(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new TableMultiCaption());
|
||||
|
||||
$violations = $scanner->scan('<table><caption>Caption 1</caption><caption>Caption 2</caption><tr><td>Data</td></tr></table>');
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('WCAG_1_3_1_TABLE_MULTI_CAPTION', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesSingleCaptionTable(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new TableMultiCaption());
|
||||
|
||||
$violations = $scanner->scan('<table><caption>Single Caption</caption><tr><td>Data</td></tr></table>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Tests\Rules;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\TableNestedBanned;
|
||||
|
||||
class TableNestedBannedTest extends TestCase
|
||||
{
|
||||
public function testFlagsNestedTable(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new TableNestedBanned());
|
||||
|
||||
$violations = $scanner->scan('<table><tr><td><table><tr><td>Nested table</td></tr></table></td></tr></table>');
|
||||
$this->assertGreaterThanOrEqual(1, count($violations));
|
||||
$this->assertSame('WCAG_1_3_1_TABLE_NESTED_BANNED', $violations[0]->ruleId);
|
||||
}
|
||||
|
||||
public function testPassesSingleTable(): void
|
||||
{
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new TableNestedBanned());
|
||||
|
||||
$violations = $scanner->scan('<table><tr><td>Single table data</td></tr></table>');
|
||||
$this->assertCount(0, $violations);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user