feat: add 15 new WCAG rules, dynamic ARIA checks and bump version to v3.1.4

This commit is contained in:
Enes Yakıştır
2026-07-30 20:34:41 +03:00
parent 164828f9f7
commit aa1b152932
31 changed files with 1138 additions and 1 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"yaknet"
],
"homepage": "https://forum.yak.net.tr",
"version": "3.1.3",
"version": "3.1.4",
"type": "library",
"license": "MIT",
"authors": [
+38
View File
@@ -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;
}
}
+30
View File
@@ -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;
}
}
+54
View File
@@ -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.'
);
}
}
+35
View File
@@ -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;
}
}
+55
View File
@@ -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.'
);
}
}
+49
View File
@@ -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.'
);
}
}
+58
View File
@@ -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.'
);
}
}
+35
View File
@@ -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;
}
}
+42
View File
@@ -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;
}
}
+55
View File
@@ -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;
}
}
+46
View File
@@ -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;
}
}
+34
View File
@@ -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;
}
}
+59
View File
@@ -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)
);
}
}
+35
View File
@@ -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;
}
}
+41
View File
@@ -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;
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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,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);
}
}
+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\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,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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}
+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\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);
}
}