Skip to content

Commit 5ff2190

Browse files
committed
tests: add partial fixtures and extend page-level tests; fix rules to avoid false positives (ignore hidden inputs, page-level detection)
1 parent 995fa64 commit 5ff2190

13 files changed

Lines changed: 127 additions & 19 deletions

AGENTS.md

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Rules/Forms/InputLabelRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
3131
return;
3232
}
3333

34+
// Ignore hidden inputs: they don't require accessible labels
35+
if (preg_match('/\btype\s*=\s*["\']hidden["\']/i', $opening)) {
36+
return;
37+
}
38+
3439
// Id present?
3540
$id = null;
3641
if (preg_match('/\bid\s*=\s*(?:"|\')([^"\']+)(?:"|\')/i', $opening, $m)) {

src/Rules/Structure/LandmarkRule.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,26 @@ public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
2525
return;
2626
}
2727

28-
// If we encounter the end of head/body start, and haven't seen
29-
// a main landmark previously, emit a missing landmark warning.
30-
if (str_contains($value, '<body') || str_contains($value, '</head>')) {
31-
// Simple approach: scan a small window ahead to see if a main is present
32-
$look = $this->collectUntil($tokenIndex, $tokens, '<main', 200);
33-
if (!str_contains($look, '<main') && !preg_match('/role\s*=\s*["\']main["\']/i', $look)) {
34-
// Emit at the start of the file for consistency with other
35-
// page-level rules (tests expect line 1:1 identifiers).
36-
$first = $tokens->get(0);
37-
$emit('Page should include a main landmark', $first, 'Landmark.MissingMain');
38-
}
28+
// This rule is page-level. Only evaluate once per file (at tokenIndex 0)
29+
// and only if the content looks like a full HTML page (contains
30+
// a <body> or a <!DOCTYPE). This avoids flagging fragments/partials.
31+
if (0 !== $tokenIndex) {
32+
return;
33+
}
34+
35+
$full = $this->getFullContent($tokens);
36+
37+
// If this looks like a fragment (no body/doctype), skip evaluation
38+
if (!str_contains($full, '<body') && !str_contains(strtoupper($full), '<!DOCTYPE')) {
39+
return;
3940
}
41+
42+
// Scan the full content for a main landmark or role="main"
43+
if (str_contains($full, '<main') || preg_match('/role\s*=\s*["\']main["\']/i', $full)) {
44+
return;
45+
}
46+
47+
$first = $tokens->get(0);
48+
$emit('Page should include a main landmark', $first, 'Landmark.MissingMain');
4049
}
4150
}

src/Rules/Structure/SkipLinkRule.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,24 @@ public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
1818
return;
1919
}
2020

21-
// Only perform a single page-level scan (when invoked on the first
22-
// token) to detect absence of skip links. This avoids emitting the
23-
// same violation multiple times.
21+
// Page-level rule: only run once (tokenIndex 0) and only for full
22+
// pages (containing <body> or <!DOCTYPE). This avoids reporting on
23+
// partials/components.
2424
if (0 !== $tokenIndex) {
2525
return;
2626
}
2727

2828
$content = $this->getFullContent($tokens);
2929

30-
if (preg_match('/href\s*=\s*["\"]#([^"\']+)["\"][^>]*>.*?skip/i', $content)) {
30+
if (!str_contains($content, '<body') && !str_contains(strtoupper($content), '<!DOCTYPE')) {
3131
return;
3232
}
3333

34-
if (preg_match('/href\s*=\s*["\"]#(main|content)["\"][^>]*>/i', $content)) {
34+
if (preg_match('/href\s*=\s*["\']#([^"\']+)["\'][^>]*>.*?skip/i', $content)) {
35+
return;
36+
}
37+
38+
if (preg_match('/href\s*=\s*["\']#(main|content)["\'][^>]*>/i', $content)) {
3539
return;
3640
}
3741

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
html<input type="hidden" name="token" value="abc">
4+
</body>
5+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
<input type="text" aria-label="{{ dynamic_label }}">
4+
</body>
5+
</html>

tests/Rules/Forms/InputLabelRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,11 @@ public static function provideFixtures(): iterable
3636
__DIR__.'/Fixtures/invalid/input_no_label.html.twig',
3737
['InputLabel.InputLabel.MissingLabel:3:1' => 'Input element must have an associated <label> or an aria-label.'],
3838
];
39+
40+
// Hidden inputs should not trigger the rule
41+
yield 'input hidden' => [__DIR__.'/Fixtures/valid/input_hidden.html.twig', []];
42+
43+
// Dynamic aria-label (Twig var) should be considered present
44+
yield 'input with aria variable' => [__DIR__.'/Fixtures/valid/input_with_aria_variable.html.twig', []];
3945
}
4046
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
</head>
5+
<body>
6+
<header>Nav</header>
7+
<div>Content without main</div>
8+
</body>
9+
</html>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{# partial component - should not be treated as a full page #}
2+
<div class="card">{{ content }}</div>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{# partial fragment with no <html> tag - should be valid for page-level rules #}
2+
<div class="snippet">{{ title }}</div>

0 commit comments

Comments
 (0)