Skip to content

[#658] Added suite-level accessibility aggregate report.#661

Merged
AlexSkrypnyk merged 8 commits into
mainfrom
feature/658-a11y-aggregate
Jun 20, 2026
Merged

[#658] Added suite-level accessibility aggregate report.#661
AlexSkrypnyk merged 8 commits into
mainfrom
feature/658-a11y-aggregate

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Jun 20, 2026

Copy link
Copy Markdown
Member

Closes #658

Summary

Adds a single suite-level accessibility report to AccessibilityTrait, written once after the whole Behat suite via a static #[AfterSuite] hook. Each scenario still writes its own per-scenario HTML and JUnit reports; the new aggregate collects and de-duplicates findings across every scenario into one cross-page overview. It is written to accessibility_report_<YYYYMMDD_HHMMSS>.html in the configured report directory - one timestamped file per run, so a run never overwrites a previous one. The work reuses the report directory and URL-formatting helpers from #657 and #659, so consumer overrides of those are honoured.

Changes

Accumulation

  • $accessibilityAggregate (process-global) collects each scenario's results; $accessibilityAggregateReportDir captures the resolved report directory from the instance phase for the static renderer.
  • accessibilityAggregateCapture() records each scenario's formatted results (URLs passed through accessibilityFormatUrl()) in accessibilityFinalizeScenario(), before the gate throw, so a scenario that fails the gate is still represented.
  • accessibilityAggregateReset() (#[BeforeSuite]) clears the statics so a second suite in the same process starts clean.

Calculation - one method per concern

  • accessibilityAggregatePages() de-duplicates assessed pages by URL, keyed by feature > scenario so same-titled scenarios keep their provenance.
  • accessibilityAggregateRollup() rolls violations up by rule, tallies by impact, and sorts by severity then affected-element count.
  • accessibilityAggregateFindings() flattens findings and stringifies element targets.
  • accessibilityAggregateData() is the single calculation entry point: it calls the above and assembles one render-ready data array (generated, page_count, scenario_count, total_violations, totals, pages, rules, scenarios).

Rendering - one method

  • accessibilityRenderAggregate(array $data) is the single rendering entry point. It receives the prepared data array and produces the entire HTML document: summary cards, a de-duplicated "Pages assessed" table with per-rule chips, a severity-sorted "Violations by rule" rollup, full per-scenario detail, and the CSS. Override this one method to restyle the whole report without touching any aggregation logic.

Reporting and helpers

  • accessibilityAggregateRender() (#[AfterSuite]) and accessibilityWriteAggregateReport() resolve the timestamp once - it drives both the filename and the in-page "generated" line - and write the report.
  • accessibilityBlankUrls() (shared, overridable) adds data:, to the blank-URL skip list, used by both per-step auto-assessment and the aggregate.
  • accessibilityStringifyTarget() is promoted to protected static so the static renderer can reuse it.
  • Trait methods are grouped by concern: all lifecycle hooks together, the format helpers together, and the aggregate calculation + render block together.

Tests

  • PHPUnit unit tests cover each calculation method, the single render method (driven directly from a hand-built data array), the writer (timestamped filename), the capture/reset hooks, and edge cases (clean run, empty helpUrl, blank-URL skipping).
  • A Behat @trait:AccessibilityTrait integration scenario asserts the timestamped accessibility_report_*.html is written after a real sub-process suite run, via a new generic a file matching "<glob>" should exist step.

Before / After

The per-scenario "single page" report machinery is unchanged. This PR adds the suite aggregate and wires it to reuse the same small helpers (accessibilityFormatUrl(), accessibilityStringifyTarget(), accessibilityBlankUrls()) rather than re-implementing them.

Before - only the per-scenario "single page" report exists:

#[AfterScenario] accessibilityFinalizeScenario()
   |
   |-- accessibilityRenderHtml()                  ->  <feature>__<scenario>.html
   |      |-- accessibilityRenderHtmlPage()            page wrapper + CSS
   |      `-- accessibilityRenderHtmlSections()        one section per visited URL
   |             |-- accessibilityFormatUrl()              URL -> path
   |             `-- accessibilityRenderIssueList()        violations + incomplete
   |                    `-- accessibilityStringifyTarget()     flatten node target
   |
   `-- accessibilityRenderJunit()                 ->  junit-<feature>__<scenario>.xml
          |-- accessibilityFormatUrl()
          `-- accessibilityStringifyTarget()

formatUrl() and stringifyTarget() are instance helpers used only by this
per-scenario path. There is no cross-page / suite-level report.

After - the per-scenario report is unchanged; a suite aggregate is added, and both reports reuse the same helpers:

shared helpers (reused by both reports)
   accessibilityFormatUrl()        URL -> path
   accessibilityStringifyTarget()  flatten node target   (promoted to static so the
   accessibilityBlankUrls()        blank-tab skip list      static aggregate can call it)

#[AfterScenario] accessibilityFinalizeScenario()
   |
   |-- accessibilityRenderHtml() + accessibilityRenderJunit()  ->  per-scenario files
   |      `-- reuse formatUrl(), stringifyTarget()                  (unchanged)
   |
   `-- accessibilityAggregateCapture()                         [NEW]
          `-- reuse formatUrl(); append results to a static accumulator
                                                                      |
                                                                      v
#[AfterSuite] accessibilityAggregateRender()                    [NEW]
   |
   `-- accessibilityWriteAggregateReport()  ->  accessibility_report_<YYYYMMDD_HHMMSS>.html
          |
          |-- accessibilityAggregateData()              CALCULATION (one entry point)
          |      |-- accessibilityAggregatePages()      reuse blankUrls()
          |      |-- accessibilityAggregateRollup()     reuse stringifyTarget()
          |      `-- accessibilityAggregateFindings()   reuse stringifyTarget()
          |      returns $data = [generated, totals, pages, rules, scenarios, ...]
          |
          `-- accessibilityRenderAggregate($data)       RENDERING (one method -> full HTML)

Screenshots

Accessibility aggregate report

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

AccessibilityTrait gains suite-level aggregate reporting: a static accumulator collects normalized per-scenario results, an AfterSuite hook renders a de-duplicated timestamped HTML report with summary cards, per-URL tables, and rule rollups. accessibilityStringifyTarget is refactored to protected static. Blank URL filtering is centralized via accessibilityBlankUrls() to include data: URIs. Aggregate capture runs before the automatic gate so gated scenarios still appear in the aggregate. PHPUnit and Behat tests cover the new behavior, and STEPS.md documents it.

Changes

Suite-level accessibility aggregate reporting

Layer / File(s) Summary
accessibilityStringifyTarget refactored to static
src/AccessibilityTrait.php
accessibilityStringifyTarget is changed from a protected instance method to protected static. All call sites in the automatic gate-message HTML generation (including incomplete findings), per-scenario HTML rendering, and JUnit rendering are updated to use static:: dispatch. JUnit testcase failure message target fallback formatting is adjusted.
Process-global state and trait infrastructure
src/AccessibilityTrait.php
Adds AfterSuite hook import. Introduces static properties for the per-process aggregate accumulator and resolved report directory. Updates trait documentation to describe cross-page aggregate reporting, de-duplication, rollup by rule, per-run behavior, and per-process reporting under parallel Behat. Adds #[BeforeSuite] reset hook to clear aggregate state between suite runs.
Blank URL centralization and aggregate capture ordering
src/AccessibilityTrait.php
Adds accessibilityBlankUrls() helper centralizing blank-tab URL detection to include data:,, about:blank, and empty string. Automatic-mode URL skipping now uses the shared helper. Scenario finalization is reordered so aggregate capture runs before automatic accessibility gate enforcement, ensuring gated scenarios still enter the aggregate.
Aggregate HTML rendering and AfterSuite hook
src/AccessibilityTrait.php
Implements suite-level aggregate reporting: accessibilityAggregateReset() and accessibilityAggregateRender() static hooks, aggregate capture formatting normalized results with feature/scenario/report-directory metadata, full HTML generation with summary cards by severity, de-duplicated per-URL table with path-only formatting, rule rollups sorted by impact then element count, per-scenario detail sections with incomplete findings, and timestamped file writing via accessibilityWriteAggregateReport(). Blank URLs are defensively skipped during page aggregation and scenario detail rendering.
Documentation
STEPS.md
Expanded documentation for AccessibilityTrait describing per-scenario HTML and JUnit outputs, post-suite timestamped cross-page HTML aggregate report, de-duplication and violation rollup by rule, per-run non-overwriting behavior, and per-process reporting under parallel Behat.
PHPUnit tests
tests/phpunit/src/AccessibilityTraitTest.php
tearDown() calls accessibilityAggregateReset() to clear aggregate state. dataProviderFormatUrl() adds a fixture asserting URL fragments are preserved. Comprehensive aggregate test suite covers HTML rendering (summary cards, severity chips, URL tables, axe rule doc links, scenario config fields, headings, incomplete findings, escaped HTML), rule sorting by severity then element count, path-only URL formatting, page deduplication and blank URL skipping, rollup tally computation, timestamped file writing (including empty no-op), AfterSuite hook rendering, filename format validation, clean-run rendering, help-doc link omission when helpUrl is empty, reset state clearing, and capture recording with feature/scenario/directory metadata. AccessibilityTraitTestImplementation exposes public wrapper methods for aggregate state access, rendering, page aggregation, rollup computation, file writing, and capture.
Behat integration tests and step support
tests/behat/bootstrap/BehatCliContext.php, tests/behat/features/accessibility.feature
BehatCliContext gains fileMatchingShouldExist($pattern) step handler that uses glob() to verify at least one file matches a provided pattern. A new @trait:AccessibilityTrait Behat scenario verifies aggregate report generation: visits a violations page then a clean page, runs behat --no-colors, asserts the suite passes, and verifies an accessibility_report_*.html file is generated in the expected location.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • drevops/behat-steps#660: Both PRs modify src/AccessibilityTrait.php to change how accessibility report paths/directories are captured and used in rendering, affecting the same report output code pathways.

Poem

🐇 Hippity-hop through each page I roam,
Collecting violations far from home.
After the suite, I roll them up tight—
One timestamped report to bring them to light.
De-duplicated, sorted by rule and by might,
The aggregate bunny has made everything right! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a suite-level accessibility aggregate report, which is the primary objective of the PR.
Linked Issues check ✅ Passed All coding objectives from issue #658 are met: static accumulator captures results, AfterSuite hook renders combined report, de-duplication across scenarios, severity-based organization, and override points for customization.
Out of Scope Changes check ✅ Passed All changes directly support aggregate reporting or supporting infrastructure (blank URL extraction, stringification refactoring, test coverage for new functionality). No extraneous modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/658-a11y-aggregate

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/AccessibilityTrait.php`:
- Around line 1061-1067: The `accessibilityAggregatePages()` method uses an `if
(!isset($pages[$url]))` guard that prevents processing of duplicate URLs,
causing subsequent results for the same URL to be ignored. Refactor this logic
to merge results when a URL is encountered multiple times: for duplicate URLs,
append violations to the existing array, and accumulate the counts for
incomplete and passes fields instead of only initializing them on first
occurrence. This ensures all accessibility findings for a given URL are properly
aggregated regardless of the order in which they appear in the results.
- Around line 1245-1253: The docs link in the meta section is always rendered
even when helpUrl is empty, resulting in broken href="" links. Modify the string
formatting to conditionally include the anchor tag only when the helpUrl from
the rule array is not empty. If helpUrl is empty, either omit the anchor tag
entirely or render plain text instead. This check should be applied to the
htmlspecialchars((string) $rule['helpUrl'], ENT_QUOTES) portion of the sprintf
call to prevent generating invalid links in the aggregate report.

In `@STEPS.md`:
- Around line 81-86: The blockquote lines in STEPS.md contain multiple spaces
after the > character, which violates the MD027 markdownlint rule that requires
exactly one space after the blockquote marker. Remove the extra space after each
> symbol in the blockquote lines (the lines containing <br/><br/> and the
Reporting description) so that each line has only a single space between the >
and the content.

In `@tests/behat/features/accessibility.feature`:
- Around line 94-99: The PyString (multi-line string) in the
accessibility.feature file uses triple double quotes (""") but according to
coding guidelines for `@trait` scenarios, it should use triple single quotes (''')
instead. Locate the PyString block containing the "Given I visit" and "When I
visit" steps, and replace both the opening delimiter on line 94 and closing
delimiter on line 99 from """ to '''.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5df17ad-5eba-42a7-9e1d-bf612fb724e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab0cbc and b76ace1.

📒 Files selected for processing (4)
  • STEPS.md
  • src/AccessibilityTrait.php
  • tests/behat/features/accessibility.feature
  • tests/phpunit/src/AccessibilityTraitTest.php

Comment thread src/AccessibilityTrait.php
Comment thread src/AccessibilityTrait.php Outdated
Comment thread STEPS.md Outdated
Comment thread tests/behat/features/accessibility.feature
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.55752% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.80%. Comparing base (1ab0cbc) to head (b2617a4).

Files with missing lines Patch % Lines
src/AccessibilityTrait.php 99.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #661      +/-   ##
==========================================
+ Coverage   96.60%   96.80%   +0.19%     
==========================================
  Files          46       46              
  Lines        3537     3757     +220     
==========================================
+ Hits         3417     3637     +220     
  Misses        120      120              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/phpunit/src/AccessibilityTraitTest.php (1)

381-384: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Expand blank-URL fixture to include data:, to match the trait contract.

The aggregate blank-URL contract includes data:,, but this fixture currently only exercises about:blank. Adding a data:, result here (or a dedicated case) would close that regression gap.

Suggested test fixture tweak
           [
             'url' => 'about:blank',
             'rules' => 'wcag2a',
             'result' => ['violations' => [], 'incomplete' => [], 'passes' => []],
           ],
+          [
+            'url' => 'data:,',
+            'rules' => 'wcag2a',
+            'result' => ['violations' => [], 'incomplete' => [], 'passes' => []],
+          ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/phpunit/src/AccessibilityTraitTest.php` around lines 381 - 384, The
blank-URL test fixture in AccessibilityTraitTest.php is incomplete and only
covers the `about:blank` case. Add an additional test fixture case for the
`data:,` URL to match the full blank-URL contract supported by the trait. The
new case should follow the same structure as the existing `about:blank` fixture
with the same expected results (empty violations, incomplete, and passes arrays)
but with the URL set to `data:,` instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/phpunit/src/AccessibilityTraitTest.php`:
- Around line 381-384: The blank-URL test fixture in AccessibilityTraitTest.php
is incomplete and only covers the `about:blank` case. Add an additional test
fixture case for the `data:,` URL to match the full blank-URL contract supported
by the trait. The new case should follow the same structure as the existing
`about:blank` fixture with the same expected results (empty violations,
incomplete, and passes arrays) but with the URL set to `data:,` instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c02e2f8a-a591-479b-8636-76387c2f510e

📥 Commits

Reviewing files that changed from the base of the PR and between c043743 and 2ab9744.

📒 Files selected for processing (2)
  • src/AccessibilityTrait.php
  • tests/phpunit/src/AccessibilityTraitTest.php

@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Jun 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/AccessibilityTrait.php`:
- Line 1158: The current implementation in the implode statement at line 1158
uses only scenario names as keys via array_keys($page['scenarios']), which
causes multiple features with identical scenario titles to collapse into a
single entry. Instead of directly using array_keys(), create a composite key
that combines feature information with the scenario identifier before imploding
to preserve the provenance of where each feature originated. This ensures that
the same scenario title from different features is tracked as separate entries
in the scenarios list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 372923cc-3a76-4eeb-a38c-c94516fd8726

📥 Commits

Reviewing files that changed from the base of the PR and between 35eb23c and 297d16e.

📒 Files selected for processing (2)
  • src/AccessibilityTrait.php
  • tests/phpunit/src/AccessibilityTraitTest.php

Comment thread src/AccessibilityTrait.php
@AlexSkrypnyk AlexSkrypnyk merged commit b56cb57 into main Jun 20, 2026
14 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/658-a11y-aggregate branch June 20, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs review Pull request needs a review from assigned developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a cross-page accessibility report aggregated across the whole suite

1 participant