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
feat: add 15 new WCAG rules, dynamic ARIA checks and bump version to v3.1.4
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"yaknet"
|
"yaknet"
|
||||||
],
|
],
|
||||||
"homepage": "https://forum.yak.net.tr",
|
"homepage": "https://forum.yak.net.tr",
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
|||||||
@@ -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,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,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,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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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\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,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,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\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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user