feat(v3.2.7): expand WCAG ruleset to 127 rules, add HTML syntax linter, single-pass rule engine, template preprocessor, and accessibility analytics

This commit is contained in:
Enes Yakıştır
2026-08-16 22:00:30 +03:00
parent 9a6784ad19
commit 2de1f34878
93 changed files with 4879 additions and 305 deletions
@@ -0,0 +1,35 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Analytics;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Analytics\AccessibilityTelemetry;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class AccessibilityTelemetryTest extends TestCase
{
private AccessibilityTelemetry $telemetry;
protected function setUp(): void
{
$this->telemetry = new AccessibilityTelemetry();
}
public function testComputesDefectDensityAndRatios(): void
{
$violations = [
new Violation(ruleId: 'R1', message: 'M1', severity: Severity::ERROR, standard: WCAGStandard::A),
new Violation(ruleId: 'R2', message: 'M2', severity: Severity::WARNING, standard: WCAGStandard::AA),
];
$metrics = $this->telemetry->computeMetrics($violations, 2000);
$this->assertSame(2, $metrics['total_violations']);
$this->assertSame(1, $metrics['error_count']);
$this->assertSame(1, $metrics['warning_count']);
$this->assertSame(1.0, $metrics['defect_density_per_kloc']); // 2 violations / 2 kloc = 1.0
$this->assertSame(50.0, $metrics['critical_blockers_ratio']); // 1 / 2 = 50%
}
}
@@ -0,0 +1,48 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Analytics;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Analytics\ComplianceMatrixGenerator;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class ComplianceMatrixGeneratorTest extends TestCase
{
private ComplianceMatrixGenerator $generator;
protected function setUp(): void
{
$this->generator = new ComplianceMatrixGenerator();
}
public function testGeneratesMatrixSuccessfully(): void
{
$violations = [
new Violation(
ruleId: 'WCAG_1_1_1_ALT',
message: 'Image alt missing',
severity: Severity::ERROR,
standard: WCAGStandard::A
),
new Violation(
ruleId: 'WCAG_2_4_4_LINK_TEXT',
message: 'Link text empty',
severity: Severity::ERROR,
standard: WCAGStandard::A
),
];
$matrix = $this->generator->generate($violations, 127);
$this->assertArrayHasKey('overall_compliance_percentage', $matrix);
$this->assertArrayHasKey('principles', $matrix);
$this->assertArrayHasKey('levels', $matrix);
$this->assertSame(1, $matrix['principles']['Perceivable']['violations']);
$this->assertSame(1, $matrix['principles']['Operable']['violations']);
$this->assertSame(0, $matrix['principles']['Understandable']['violations']);
$this->assertSame('Pass', $matrix['principles']['Understandable']['status']);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Core;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Locator\PreciseElementLocator;
class PreciseElementLocatorTest extends TestCase
{
private PreciseElementLocator $locator;
protected function setUp(): void
{
$this->locator = new PreciseElementLocator();
}
public function testLocatesExactMatch(): void
{
$content = "<html>\n<body>\n<div class=\"container\">\n<img src=\"logo.png\">\n</div>\n</body>\n</html>";
$snippet = '<img src="logo.png">';
$loc = $this->locator->locate($content, $snippet);
$this->assertSame(4, $loc['line']);
}
public function testLocatesAttributeTokenMatch(): void
{
$content = "<div>\n<button id=\"save-btn\" class=\"btn btn-primary\">\nSave\n</button>\n</div>";
// Snippet with different whitespace/attr arrangement
$snippet = '<button class="btn btn-primary" id="save-btn">';
$loc = $this->locator->locate($content, $snippet);
$this->assertSame(2, $loc['line']);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Core;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\RuleRegistry;
use YakNet\AccessibilityConsole\Rules\RuleLevels;
class RuleRegistryTest extends TestCase
{
public function testValidateRuleSetHasZeroDuplicates(): void
{
$rules = RuleLevels::getRulesForLevel(9);
$result = RuleRegistry::validate($rules);
$this->assertTrue($result['valid'], 'Rule validation failed: ' . implode("\n", $result['errors']));
$this->assertEmpty($result['errors']);
$this->assertGreaterThanOrEqual(120, $result['count']);
}
public function testGetMetadataReturnsAccurateData(): void
{
$rules = RuleLevels::getRulesForLevel(1);
$firstRule = $rules[0];
$meta = RuleRegistry::getMetadata($firstRule);
$this->assertArrayHasKey('id', $meta);
$this->assertArrayHasKey('class', $meta);
$this->assertArrayHasKey('severity', $meta);
$this->assertArrayHasKey('standard', $meta);
$this->assertArrayHasKey('level', $meta);
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Parser;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Parser\TemplatePreprocessor;
class TemplatePreprocessorTest extends TestCase
{
private TemplatePreprocessor $preprocessor;
protected function setUp(): void
{
$this->preprocessor = new TemplatePreprocessor();
}
public function testPreprocessesBladeTemplate(): void
{
$blade = "<div>\n@if(true)\n<h1>{{ title }}</h1>\n@endif\n</div>";
$processed = $this->preprocessor->preprocess($blade, 'views/user.blade.php');
$this->assertStringNotContainsString('@if', $processed);
$this->assertStringNotContainsString('{{', $processed);
$this->assertStringContainsString('<h1>', $processed);
// Verify line count is preserved
$this->assertSame(substr_count($blade, "\n"), substr_count($processed, "\n"));
}
public function testPreprocessesTwigTemplate(): void
{
$twig = "<div>\n{% if logged_in %}\n<p>{{ user.email }}</p>\n{% endif %}\n</div>";
$processed = $this->preprocessor->preprocess($twig, 'templates/user.twig');
$this->assertStringNotContainsString('{%', $processed);
$this->assertStringNotContainsString('{{', $processed);
$this->assertStringContainsString('<p>', $processed);
// Verify line count is preserved
$this->assertSame(substr_count($twig, "\n"), substr_count($processed, "\n"));
}
public function testPreprocessesPhpTemplate(): void
{
$php = "<div>\n<?php if (true): ?>\n<button><?= \$label ?></button>\n<?php endif; ?>\n</div>";
$processed = $this->preprocessor->preprocess($php, 'views/button.php');
$this->assertStringNotContainsString('<?php', $processed);
$this->assertStringNotContainsString('<?=', $processed);
$this->assertStringContainsString('<button>', $processed);
// Verify line count is preserved
$this->assertSame(substr_count($php, "\n"), substr_count($processed, "\n"));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AccesskeyDuplicate;
class AccesskeyDuplicateTest extends TestCase
{
public function testFlagsDuplicateAccesskeys(): void
{
$scanner = new Scanner();
$scanner->addRule(new AccesskeyDuplicate());
$violations = $scanner->scan('<div><a href="/" accesskey="h">Home</a><a href="/help" accesskey="h">Help</a></div>');
$this->assertCount(2, $violations);
$this->assertSame('WCAG_2_1_1_ACCESSKEY_DUPLICATE', $violations[0]->ruleId);
}
public function testPassesUniqueAccesskeys(): void
{
$scanner = new Scanner();
$scanner->addRule(new AccesskeyDuplicate());
$violations = $scanner->scan('<div><a href="/" accesskey="h">Home</a><a href="/help" accesskey="p">Help</a></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaBrailleEquivalent;
class AriaBrailleEquivalentTest extends TestCase
{
public function testFlagsBrailleLabelWithoutStandardName(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaBrailleEquivalent());
$violations = $scanner->scan('<div><button aria-braillelabel="btn"></button></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_BRAILLE', $violations[0]->ruleId);
}
public function testPassesBrailleLabelWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaBrailleEquivalent());
$violations = $scanner->scan('<div><button aria-braillelabel="btn" aria-label="Submit Form">Submit</button></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaCurrentValid;
class AriaCurrentValidTest extends TestCase
{
public function testFlagsInvalidAriaCurrent(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaCurrentValid());
$violations = $scanner->scan('<div><a href="/home" aria-current="active">Home</a></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_CURRENT', $violations[0]->ruleId);
}
public function testPassesValidAriaCurrent(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaCurrentValid());
$violations = $scanner->scan('<div><a href="/home" aria-current="page">Home</a></div>');
$this->assertCount(0, $violations);
}
public function testPassesBooleanAriaCurrent(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaCurrentValid());
$violations = $scanner->scan('<div><a href="/step2" aria-current="true">Step 2</a></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaDisabledFocusable;
class AriaDisabledFocusableTest extends TestCase
{
public function testFlagsDisabledWithPositiveTabindex(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaDisabledFocusable());
$violations = $scanner->scan('<div><button disabled tabindex="0">Disabled Button</button></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_DISABLED', $violations[0]->ruleId);
}
public function testPassesStandardDisabled(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaDisabledFocusable());
$violations = $scanner->scan('<div><button disabled>Disabled Button</button></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaErrorMessage;
class AriaErrorMessageTest extends TestCase
{
public function testFlagsMissingTargetElement(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaErrorMessage());
$violations = $scanner->scan('<div><input id="email" aria-invalid="true" aria-errormessage="nonexistent_err"></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_3_3_1_ARIA_ERRORMESSAGE', $violations[0]->ruleId);
}
public function testFlagsMissingAriaInvalid(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaErrorMessage());
$violations = $scanner->scan('<div><input id="email" aria-errormessage="email_err"><span id="email_err">Error</span></div>');
$this->assertCount(1, $violations);
}
public function testPassesValidErrorMessageSetup(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaErrorMessage());
$violations = $scanner->scan('<div><input id="email" aria-invalid="true" aria-errormessage="email_err"><span id="email_err">Invalid Email Address</span></div>');
$this->assertCount(0, $violations);
}
}
@@ -0,0 +1,33 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaLabelMisuseOnGeneric;
class AriaLabelMisuseOnGenericTest extends TestCase
{
public function testFlagsGenericDivWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaLabelMisuseOnGeneric());
$html = '<div><div aria-label="Card container">Content</div></div>';
$violations = $scanner->scan($html);
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_LABEL_GENERIC', $violations[0]->ruleId);
}
public function testPassesDivWithRoleAndAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaLabelMisuseOnGeneric());
$html = '<div><div role="region" aria-label="Card container">Content</div></div>';
$violations = $scanner->scan($html);
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaProhibitedAttr;
class AriaProhibitedAttrTest extends TestCase
{
public function testFlagsAriaLabelOnPresentationRole(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaProhibitedAttr());
$violations = $scanner->scan('<div><div role="presentation" aria-label="Decorative block">Content</div></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_PROHIBITED_ATTR', $violations[0]->ruleId);
}
public function testFlagsAriaLabelledbyOnNoneRole(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaProhibitedAttr());
$violations = $scanner->scan('<div><div role="none" aria-labelledby="some_id">Content</div></div>');
$this->assertCount(1, $violations);
}
public function testPassesValidRolesWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaProhibitedAttr());
$violations = $scanner->scan('<div><div role="region" aria-label="Main Section">Content</div></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaRedundantRole;
class AriaRedundantRoleTest extends TestCase
{
public function testFlagsRedundantRoleOnNav(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaRedundantRole());
$violations = $scanner->scan('<div><nav role="navigation"><a href="/">Home</a></nav></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_REDUNDANT_ROLE', $violations[0]->ruleId);
}
public function testPassesNavWithoutRedundantRole(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaRedundantRole());
$violations = $scanner->scan('<div><nav><a href="/">Home</a></nav></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\AriaRoledescription;
class AriaRoledescriptionTest extends TestCase
{
public function testFlagsEmptyRoledescription(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaRoledescription());
$violations = $scanner->scan('<div><div role="region" aria-roledescription="">Content</div></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_ARIA_ROLEDESCRIPTION', $violations[0]->ruleId);
}
public function testFlagsRoledescriptionOnPresentation(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaRoledescription());
$violations = $scanner->scan('<div><div role="presentation" aria-roledescription="slide">Content</div></div>');
$this->assertCount(1, $violations);
}
public function testPassesValidRoledescription(): void
{
$scanner = new Scanner();
$scanner->addRule(new AriaRoledescription());
$violations = $scanner->scan('<div><div role="region" aria-roledescription="slide" aria-label="Slide 1">Slide Content</div></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\ButtonNestedInteractive;
class ButtonNestedInteractiveTest extends TestCase
{
public function testFlagsNestedAnchorInsideButton(): void
{
$scanner = new Scanner();
$scanner->addRule(new ButtonNestedInteractive());
$violations = $scanner->scan('<div><button><a href="/cart">Cart</a></button></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_BUTTON_NESTED_INTERACTIVE', $violations[0]->ruleId);
}
public function testFlagsNestedButtonInsideButton(): void
{
$scanner = new Scanner();
$scanner->addRule(new ButtonNestedInteractive());
$violations = $scanner->scan('<div><button><button>Sub action</button></button></div>');
$this->assertCount(1, $violations);
}
public function testPassesStandardButton(): void
{
$scanner = new Scanner();
$scanner->addRule(new ButtonNestedInteractive());
$violations = $scanner->scan('<div><button><span>Standard Button</span></button></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\DetailsMissingSummary;
class DetailsMissingSummaryTest extends TestCase
{
public function testFlagsDetailsWithoutSummary(): void
{
$scanner = new Scanner();
$scanner->addRule(new DetailsMissingSummary());
$violations = $scanner->scan('<details><p>Some hidden details here</p></details>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_DETAILS_SUMMARY', $violations[0]->ruleId);
}
public function testFlagsDetailsWithEmptySummary(): void
{
$scanner = new Scanner();
$scanner->addRule(new DetailsMissingSummary());
$violations = $scanner->scan('<details><summary></summary><p>Details</p></details>');
$this->assertCount(1, $violations);
}
public function testPassesDetailsWithValidSummary(): void
{
$scanner = new Scanner();
$scanner->addRule(new DetailsMissingSummary());
$violations = $scanner->scan('<details><summary>More info</summary><p>Details content</p></details>');
$this->assertCount(0, $violations);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\DraggingMovements;
class DraggingMovementsTest extends TestCase
{
public function testFlagsDraggableWithoutAlternativeControls(): void
{
$scanner = new Scanner();
$scanner->addRule(new DraggingMovements());
$html = '<div><div draggable="true" class="task-card">Task 1</div></div>';
$violations = $scanner->scan($html);
$this->assertCount(1, $violations);
$this->assertSame('WCAG_2_5_7_DRAGGING_MOVEMENTS', $violations[0]->ruleId);
}
public function testPassesDraggableWithKeyboardShortcutsOrButtons(): void
{
$scanner = new Scanner();
$scanner->addRule(new DraggingMovements());
$html = '<div><div draggable="true" aria-keyshortcuts="Control+Up Control+Down" class="task-card">Task 1</div></div>';
$violations = $scanner->scan($html);
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\DuplicateId;
class DuplicateIdTest extends TestCase
{
public function testFlagsDuplicateIds(): void
{
$scanner = new Scanner();
$scanner->addRule(new DuplicateId());
$violations = $scanner->scan('<div><div id="card">A</div><div id="card">B</div></div>');
$this->assertCount(2, $violations);
$this->assertSame('WCAG_4_1_2_DUPLICATE_ID', $violations[0]->ruleId);
}
public function testPassesUniqueIds(): void
{
$scanner = new Scanner();
$scanner->addRule(new DuplicateId());
$violations = $scanner->scan('<div><div id="card1">A</div><div id="card2">B</div></div>');
$this->assertCount(0, $violations);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\EmptyHeading;
class EmptyHeadingTest extends TestCase
{
public function testFlagsEmptyHeading(): void
{
$scanner = new Scanner();
$scanner->addRule(new EmptyHeading());
$violations = $scanner->scan('<div><h1></h1></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_EMPTY_HEADING', $violations[0]->ruleId);
}
public function testFlagsEmptyAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new EmptyHeading());
$violations = $scanner->scan('<div><h2 aria-label=" "></h2></div>');
$this->assertCount(1, $violations);
}
public function testPassesHeadingWithImageAlt(): void
{
$scanner = new Scanner();
$scanner->addRule(new EmptyHeading());
$violations = $scanner->scan('<div><h1><img src="logo.png" alt="Company Name"></h1></div>');
$this->assertCount(0, $violations);
}
public function testPassesHeadingWithSvgTitle(): void
{
$scanner = new Scanner();
$scanner->addRule(new EmptyHeading());
$violations = $scanner->scan('<div><h1><svg><title>Dashboard</title></svg></h1></div>');
$this->assertCount(0, $violations);
}
public function testPassesHeadingWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new EmptyHeading());
$violations = $scanner->scan('<div><h1 aria-label="Accessible Title"></h1></div>');
$this->assertCount(0, $violations);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\FocusNotObscured;
class FocusNotObscuredTest extends TestCase
{
public function testFlagsFixedFullWidthTopBarWithoutScrollPadding(): void
{
$scanner = new Scanner();
$scanner->addRule(new FocusNotObscured());
$html = '<div><header style="position: fixed; top: 0; left: 0; width: 100%; height: 60px;">Navbar</header></div>';
$violations = $scanner->scan($html);
$this->assertCount(1, $violations);
$this->assertSame('WCAG_2_4_11_FOCUS_NOT_OBSCURED', $violations[0]->ruleId);
}
public function testPassesStaticElements(): void
{
$scanner = new Scanner();
$scanner->addRule(new FocusNotObscured());
$html = '<div><header style="width: 100%; height: 60px;">Navbar</header></div>';
$violations = $scanner->scan($html);
$this->assertCount(0, $violations);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\FormLabel;
class FormLabelTest extends TestCase
{
public function testFlagsInputWithoutLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new FormLabel());
$violations = $scanner->scan('<div><input type="text" name="username"></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_LABEL', $violations[0]->ruleId);
}
public function testFlagsEmptyAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new FormLabel());
$violations = $scanner->scan('<div><input type="text" name="username" aria-label=" "></div>');
$this->assertCount(1, $violations);
}
public function testPassesInputWithAssociatedLabelFor(): void
{
$scanner = new Scanner();
$scanner->addRule(new FormLabel());
$violations = $scanner->scan('<div><label for="usr">Username</label><input type="text" id="usr" name="username"></div>');
$this->assertCount(0, $violations);
}
public function testPassesInputWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new FormLabel());
$violations = $scanner->scan('<div><input type="text" name="username" aria-label="Username"></div>');
$this->assertCount(0, $violations);
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\HeadingOrder;
class HeadingOrderTest extends TestCase
{
public function testFlagsSkippedHeadingLevel(): void
{
$scanner = new Scanner();
$scanner->addRule(new HeadingOrder());
$violations = $scanner->scan('<div><h1>Main Heading</h1><h3>Skipped Subheading</h3></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_HEADING', $violations[0]->ruleId);
}
public function testPassesSequentialHeadings(): void
{
$scanner = new Scanner();
$scanner->addRule(new HeadingOrder());
$violations = $scanner->scan('<div><h1>Main Heading</h1><h2>Section</h2><h3>Sub-section</h3></div>');
$this->assertCount(0, $violations);
}
public function testIsStatelessAcrossMultipleScans(): void
{
$scanner = new Scanner();
$scanner->addRule(new HeadingOrder());
// First scan ends with H3
$scanner->scan('<div><h1>H1</h1><h2>H2</h2><h3>H3</h3></div>');
// Second scan starts with H1, should not flag jump from previous scan's H3
$violations = $scanner->scan('<div><h1>Fresh Document H1</h1><h2>H2</h2></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\HtmlDirValid;
class HtmlDirValidTest extends TestCase
{
public function testFlagsInvalidDir(): void
{
$scanner = new Scanner();
$scanner->addRule(new HtmlDirValid());
$violations = $scanner->scan('<div><p dir="left">Paragraph</p></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_2_HTML_DIR', $violations[0]->ruleId);
}
public function testPassesValidDir(): void
{
$scanner = new Scanner();
$scanner->addRule(new HtmlDirValid());
$violations = $scanner->scan('<div><p dir="rtl">Arabic text</p><p dir="ltr">English</p><p dir="auto">Auto</p></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\HtmlSyntaxValid;
class HtmlSyntaxValidTest extends TestCase
{
public function testFlagsDuplicateAttributesInHtml(): void
{
$scanner = new Scanner();
$scanner->addRule(new HtmlSyntaxValid());
$violations = $scanner->scan('<div><input id="email" type="email" id="email2"></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_1_HTML_PARSING_SYNTAX', $violations[0]->ruleId);
}
public function testPassesValidHtml(): void
{
$scanner = new Scanner();
$scanner->addRule(new HtmlSyntaxValid());
$violations = $scanner->scan('<div><input id="email" type="email" name="user_email"></div>');
$this->assertCount(0, $violations);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\IframeTitle;
class IframeTitleTest extends TestCase
{
public function testFlagsIframeWithoutTitle(): void
{
$scanner = new Scanner();
$scanner->addRule(new IframeTitle());
$violations = $scanner->scan('<div><iframe src="map.html"></iframe></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_IFRAME_TITLE', $violations[0]->ruleId);
}
public function testPassesIframeWithTitle(): void
{
$scanner = new Scanner();
$scanner->addRule(new IframeTitle());
$violations = $scanner->scan('<div><iframe src="map.html" title="Office Location Map"></iframe></div>');
$this->assertCount(0, $violations);
}
public function testPassesIframeWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new IframeTitle());
$violations = $scanner->scan('<div><iframe src="map.html" aria-label="Interactive Map"></iframe></div>');
$this->assertCount(0, $violations);
}
public function testPassesAriaHiddenIframe(): void
{
$scanner = new Scanner();
$scanner->addRule(new IframeTitle());
$violations = $scanner->scan('<div><iframe src="tracker.html" aria-hidden="true"></iframe></div>');
$this->assertCount(0, $violations);
}
}
@@ -0,0 +1,33 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\InputAutocompleteAppropriate;
class InputAutocompleteAppropriateTest extends TestCase
{
public function testFlagsInappropriateAutocompleteType(): void
{
$scanner = new Scanner();
$scanner->addRule(new InputAutocompleteAppropriate());
$html = '<form><input type="number" autocomplete="email"></form>';
$violations = $scanner->scan($html);
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_5_AUTOCOMPLETE_APPROPRIATE', $violations[0]->ruleId);
}
public function testPassesAppropriateAutocompleteType(): void
{
$scanner = new Scanner();
$scanner->addRule(new InputAutocompleteAppropriate());
$html = '<form><input type="email" autocomplete="email"></form>';
$violations = $scanner->scan($html);
$this->assertCount(0, $violations);
}
}
+15 -5
View File
@@ -8,22 +8,32 @@ use YakNet\AccessibilityConsole\Rules\InputMissingAutocomplete;
class InputMissingAutocompleteTest extends TestCase
{
public function testFlagsPersonalDataInputWithoutAutocomplete(): void
public function testFlagsPasswordWithAutocompleteOff(): void
{
$scanner = new Scanner();
$scanner->addRule(new InputMissingAutocomplete());
$violations = $scanner->scan('<form><input type="email" name="user_email"></form>');
$violations = $scanner->scan('<form><input type="password" name="pwd" autocomplete="off"></form>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_5_INPUT_AUTOCOMPLETE', $violations[0]->ruleId);
$this->assertSame('WCAG_3_3_8_ACCESSIBLE_AUTH', $violations[0]->ruleId);
}
public function testPassesPersonalDataInputWithAutocomplete(): void
public function testFlagsPasteBlockingOnInput(): void
{
$scanner = new Scanner();
$scanner->addRule(new InputMissingAutocomplete());
$violations = $scanner->scan('<form><input type="email" name="user_email" autocomplete="email"></form>');
$violations = $scanner->scan('<form><input type="text" name="pin" onpaste="return false;"></form>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_3_3_8_ACCESSIBLE_AUTH', $violations[0]->ruleId);
}
public function testPassesAccessiblePasswordInput(): void
{
$scanner = new Scanner();
$scanner->addRule(new InputMissingAutocomplete());
$violations = $scanner->scan('<form><input type="password" name="pwd" autocomplete="current-password"></form>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\MediaControls;
class MediaControlsTest extends TestCase
{
public function testFlagsAudioWithoutControls(): void
{
$scanner = new Scanner();
$scanner->addRule(new MediaControls());
$violations = $scanner->scan('<div><audio src="audio.mp3"></audio></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_4_2_MEDIA_CONTROLS', $violations[0]->ruleId);
}
public function testPassesAudioWithControls(): void
{
$scanner = new Scanner();
$scanner->addRule(new MediaControls());
$violations = $scanner->scan('<div><audio src="audio.mp3" controls></audio></div>');
$this->assertCount(0, $violations);
}
public function testPassesBackgroundMutedAutoplayVideo(): void
{
$scanner = new Scanner();
$scanner->addRule(new MediaControls());
$violations = $scanner->scan('<div><video src="bg.mp4" muted autoplay loop></video></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\MetaCharsetPresent;
class MetaCharsetPresentTest extends TestCase
{
public function testFlagsMissingCharsetInHead(): void
{
$scanner = new Scanner();
$scanner->addRule(new MetaCharsetPresent());
$violations = $scanner->scan('<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Hello</h1></body></html>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_3_1_1_META_CHARSET', $violations[0]->ruleId);
}
public function testPassesWhenMetaCharsetPresent(): void
{
$scanner = new Scanner();
$scanner->addRule(new MetaCharsetPresent());
$violations = $scanner->scan('<!DOCTYPE html><html><head><meta charset="utf-8"><title>Test Page</title></head><body><h1>Hello</h1></body></html>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\MeterAccessibility;
class MeterAccessibilityTest extends TestCase
{
public function testFlagsMeterWithoutAccessibleName(): void
{
$scanner = new Scanner();
$scanner->addRule(new MeterAccessibility());
$violations = $scanner->scan('<div><meter value="0.6"></meter></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_METER_ACCESSIBLE', $violations[0]->ruleId);
}
public function testPassesMeterWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new MeterAccessibility());
$violations = $scanner->scan('<div><meter value="0.6" aria-label="Disk Usage"></meter></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\NestedInteractive;
class NestedInteractiveTest extends TestCase
{
public function testFlagsNestedLinkInsideLink(): void
{
$scanner = new Scanner();
$scanner->addRule(new NestedInteractive());
$violations = $scanner->scan('<div><a href="/outer">Outer <a href="/inner">Inner</a></a></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_4_1_2_NESTED_INTERACTIVE', $violations[0]->ruleId);
}
public function testFlagsButtonInsideLink(): void
{
$scanner = new Scanner();
$scanner->addRule(new NestedInteractive());
$violations = $scanner->scan('<div><a href="/outer"><button>Button</button></a></div>');
$this->assertCount(1, $violations);
}
public function testPassesNonNestedInteractives(): void
{
$scanner = new Scanner();
$scanner->addRule(new NestedInteractive());
$violations = $scanner->scan('<div><a href="/link">Link</a><button>Button</button></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\RoleImgAlt;
class RoleImgAltTest extends TestCase
{
public function testFlagsRoleImgWithoutAccessibleName(): void
{
$scanner = new Scanner();
$scanner->addRule(new RoleImgAlt());
$violations = $scanner->scan('<div><span role="img" class="icon-star"></span></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_1_1_ROLE_IMG', $violations[0]->ruleId);
}
public function testPassesRoleImgWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new RoleImgAlt());
$violations = $scanner->scan('<div><span role="img" class="icon-star" aria-label="Five Star Rating"></span></div>');
$this->assertCount(0, $violations);
}
public function testPassesRoleImgWithAriaHidden(): void
{
$scanner = new Scanner();
$scanner->addRule(new RoleImgAlt());
$violations = $scanner->scan('<div><span role="img" class="icon-star" aria-hidden="true"></span></div>');
$this->assertCount(0, $violations);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\SelectValidChildren;
class SelectValidChildrenTest extends TestCase
{
public function testFlagsInvalidDivInSelect(): void
{
$scanner = new Scanner();
$scanner->addRule(new SelectValidChildren());
$violations = $scanner->scan('<select><div>Option 1</div><option value="1">Option 1</option></select>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_SELECT_CHILDREN', $violations[0]->ruleId);
}
public function testPassesValidSelectOptionsAndOptgroups(): void
{
$scanner = new Scanner();
$scanner->addRule(new SelectValidChildren());
$violations = $scanner->scan('<select><optgroup label="Group"><option value="1">Opt 1</option></optgroup></select>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\TableHeaderNotEmpty;
class TableHeaderNotEmptyTest extends TestCase
{
public function testFlagsEmptyTableHeader(): void
{
$scanner = new Scanner();
$scanner->addRule(new TableHeaderNotEmpty());
$violations = $scanner->scan('<table><tr><th></th><td>Value</td></tr></table>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_TH_NOT_EMPTY', $violations[0]->ruleId);
}
public function testPassesTableHeaderWithText(): void
{
$scanner = new Scanner();
$scanner->addRule(new TableHeaderNotEmpty());
$violations = $scanner->scan('<table><tr><th>Name</th><td>John</td></tr></table>');
$this->assertCount(0, $violations);
}
public function testPassesTableHeaderWithAriaLabel(): void
{
$scanner = new Scanner();
$scanner->addRule(new TableHeaderNotEmpty());
$violations = $scanner->scan('<table><tr><th aria-label="Action buttons"></th><td>Edit</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\TableHeadersAttributeValid;
class TableHeadersAttributeValidTest extends TestCase
{
public function testFlagsInvalidHeaderId(): void
{
$scanner = new Scanner();
$scanner->addRule(new TableHeadersAttributeValid());
$violations = $scanner->scan('<table><tr><th id="h1">Header 1</th></tr><tr><td headers="nonexistent_h">Cell</td></tr></table>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_3_1_TABLE_HEADERS_ATTR', $violations[0]->ruleId);
}
public function testPassesValidHeaderId(): void
{
$scanner = new Scanner();
$scanner->addRule(new TableHeadersAttributeValid());
$violations = $scanner->scan('<table><tr><th id="h1">Header 1</th></tr><tr><td headers="h1">Cell</td></tr></table>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\TargetSizeMinimum;
class TargetSizeMinimumTest extends TestCase
{
public function testFlagsTooSmallTarget(): void
{
$scanner = new Scanner();
$scanner->addRule(new TargetSizeMinimum());
$violations = $scanner->scan('<div><button style="width: 16px; height: 16px;">X</button></div>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_2_5_8_TARGET_SIZE', $violations[0]->ruleId);
}
public function testPassesAdequateTargetSize(): void
{
$scanner = new Scanner();
$scanner->addRule(new TargetSizeMinimum());
$violations = $scanner->scan('<div><button style="width: 24px; height: 24px;">X</button></div>');
$this->assertCount(0, $violations);
}
public function testPassesElementsWithoutInlineSize(): void
{
$scanner = new Scanner();
$scanner->addRule(new TargetSizeMinimum());
$violations = $scanner->scan('<div><a href="/link">Normal Link</a></div>');
$this->assertCount(0, $violations);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Rules;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\TrackValidation;
class TrackValidationTest extends TestCase
{
public function testFlagsInvalidTrackKind(): void
{
$scanner = new Scanner();
$scanner->addRule(new TrackValidation());
$violations = $scanner->scan('<video src="v.mp4"><track kind="invalid_kind" src="track.vtt"></video>');
$this->assertCount(1, $violations);
$this->assertSame('WCAG_1_2_2_TRACK_VALIDATION', $violations[0]->ruleId);
}
public function testFlagsMissingSrclangOnCaptions(): void
{
$scanner = new Scanner();
$scanner->addRule(new TrackValidation());
$violations = $scanner->scan('<video src="v.mp4"><track kind="captions" src="track.vtt" label="English"></video>');
$this->assertCount(1, $violations);
}
public function testPassesValidTrack(): void
{
$scanner = new Scanner();
$scanner->addRule(new TrackValidation());
$violations = $scanner->scan('<video src="v.mp4"><track kind="captions" src="track.vtt" srclang="en" label="English Captions"></video>');
$this->assertCount(0, $violations);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace YakNet\AccessibilityConsole\Tests\Syntax;
use PHPUnit\Framework\TestCase;
use YakNet\AccessibilityConsole\Syntax\HtmlSyntaxLinter;
class HtmlSyntaxLinterTest extends TestCase
{
private HtmlSyntaxLinter $linter;
protected function setUp(): void
{
$this->linter = new HtmlSyntaxLinter();
}
public function testDetectsDuplicateAttributes(): void
{
$html = '<div><input type="text" id="username" class="form-control" id="duplicate_user"></div>';
$issues = $this->linter->lint($html);
$this->assertNotEmpty($issues);
$duplicateIssues = array_filter($issues, fn($i) => $i->code === 'SYNTAX_DUPLICATE_ATTR');
$this->assertCount(1, $duplicateIssues);
}
public function testDetectsInvalidSelfClosingOnNonVoidElement(): void
{
$html = '<div><div class="card" /><p>Text</p></div>';
$issues = $this->linter->lint($html);
$this->assertNotEmpty($issues);
$selfCloseIssues = array_filter($issues, fn($i) => $i->code === 'SYNTAX_INVALID_SELF_CLOSING');
$this->assertCount(1, $selfCloseIssues);
}
public function testDetectsUnclosedComments(): void
{
$html = '<div><!-- unclosed comment <p>Text</p></div>';
$issues = $this->linter->lint($html);
$this->assertNotEmpty($issues);
$commentIssues = array_filter($issues, fn($i) => $i->code === 'SYNTAX_UNCLOSED_COMMENT');
$this->assertCount(1, $commentIssues);
}
public function testDetectsMismatchedTags(): void
{
$html = '<main><section><p>Text</section></main>';
$issues = $this->linter->lint($html);
$this->assertNotEmpty($issues);
$mismatchIssues = array_filter($issues, fn($i) => in_array($i->code, ['SYNTAX_MISMATCHED_TAG', 'SYNTAX_UNCLOSED_TAG']));
$this->assertNotEmpty($mismatchIssues);
}
public function testPassesValidHtml5(): void
{
$html = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Test</title></head><body><main><h1>Hello</h1><p>World</p></main></body></html>';
$issues = $this->linter->lint($html);
$this->assertEmpty($issues);
}
}