diff --git a/STEPS.md b/STEPS.md index 4e4fddd2..f55029bc 100644 --- a/STEPS.md +++ b/STEPS.md @@ -526,6 +526,62 @@ Then the element "#content" should be centered in the viewport +
+ @Then the element :selector should have keyboard focus + +
+Assert that the element has keyboard focus +

+ +```gherkin +Then the element "#edit-name" should have keyboard focus + +``` + +
+ +
+ @Then the element :selector should not have keyboard focus + +
+Assert that the element does not have keyboard focus +

+ +```gherkin +Then the element "#edit-name" should not have keyboard focus + +``` + +
+ +
+ @Then the element :selector should have a visible focus outline + +
+Assert that the element has a visible focus indicator +

+ +```gherkin +Then the element "#edit-name" should have a visible focus outline + +``` + +
+ +
+ @Then the element :selector should not have a visible focus outline + +
+Assert that the element does not have a visible focus indicator +

+ +```gherkin +Then the element "#decorative-icon" should not have a visible focus outline + +``` + +
+
@Then the element :selector should be displayed diff --git a/src/ElementTrait.php b/src/ElementTrait.php index f8558f43..e91ae456 100644 --- a/src/ElementTrait.php +++ b/src/ElementTrait.php @@ -379,6 +379,158 @@ public function elementFocus(string $selector): void { $this->elementExecuteJs($selector, '{{ELEMENT}}.focus();'); } + /** + * Assert that the element has keyboard focus. + * + * Verifies that the element matched by the selector is the current + * `document.activeElement`. This is the canonical check for tab-order tests, + * skip-link behaviour, modal focus traps, autofocus, and focus-after-action + * flows. + * + * @code + * Then the element "#edit-name" should have keyboard focus + * @endcode + * + * @javascript + */ + #[Then('the element :selector should have keyboard focus')] + public function elementAssertHasKeyboardFocus(string $selector): void { + $this->elementAssertKeyboardFocus($selector, FALSE); + } + + /** + * Assert that the element does not have keyboard focus. + * + * @code + * Then the element "#edit-name" should not have keyboard focus + * @endcode + * + * @javascript + */ + #[Then('the element :selector should not have keyboard focus')] + public function elementAssertNotHasKeyboardFocus(string $selector): void { + $this->elementAssertKeyboardFocus($selector, TRUE); + } + + /** + * Assert that the element has a visible focus indicator. + * + * Verifies that the element renders a visible focus indicator via either + * a CSS outline (non-`none` outline-style with a width greater than 0) or + * a non-`none` box-shadow. Guards WCAG 2.4.7 (Focus Visible) and catches + * accidental `outline: none` regressions introduced by stylesheet changes. + * + * @code + * Then the element "#edit-name" should have a visible focus outline + * @endcode + * + * @javascript + */ + #[Then('the element :selector should have a visible focus outline')] + public function elementAssertHasVisibleFocusOutline(string $selector): void { + $this->elementAssertVisibleFocusOutline($selector, FALSE); + } + + /** + * Assert that the element does not have a visible focus indicator. + * + * @code + * Then the element "#decorative-icon" should not have a visible focus outline + * @endcode + * + * @javascript + */ + #[Then('the element :selector should not have a visible focus outline')] + public function elementAssertNotHasVisibleFocusOutline(string $selector): void { + $this->elementAssertVisibleFocusOutline($selector, TRUE); + } + + /** + * Assert keyboard focus state for an element. + * + * @param string $selector + * The CSS selector. + * @param bool $is_inverted + * Whether to assert that the element does not have keyboard focus. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function elementAssertKeyboardFocus(string $selector, bool $is_inverted): void { + $element = $this->getSession()->getPage()->find('css', $selector); + + if (!$element) { + throw new ElementNotFoundException($this->getSession()->getDriver(), 'element', 'css', $selector); + } + + $script = <<elementExecuteJs($selector, $script); + + if (!$is_inverted) { + if ($result === '__OK__') { + return; + } + + $message = $result === '__NONE__' + ? sprintf('Expected element "%s" to have keyboard focus, but no element is focused.', $selector) + : sprintf('Expected element "%s" to have keyboard focus, but focus is on: %s', $selector, $result); + throw new ExpectationException($message, $this->getSession()->getDriver()); + } + + if ($result === '__OK__') { + throw new ExpectationException(sprintf('Expected element "%s" to not have keyboard focus, but it does.', $selector), $this->getSession()->getDriver()); + } + } + + /** + * Assert visible focus indicator state for an element. + * + * An element is considered to have a visible focus indicator when either + * its computed outline has a non-`none` style with a width greater than 0, + * or its computed box-shadow is not `none`. + * + * @param string $selector + * The CSS selector. + * @param bool $is_inverted + * Whether to assert that the element does not have a visible focus + * indicator. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + protected function elementAssertVisibleFocusOutline(string $selector, bool $is_inverted): void { + $element = $this->getSession()->getPage()->find('css', $selector); + + if (!$element) { + throw new ElementNotFoundException($this->getSession()->getDriver(), 'element', 'css', $selector); + } + + $script = <<elementExecuteJs($selector, $script); + [$outline_style, $outline_width, $box_shadow] = explode('|', $result, 3); + + $has_outline = $outline_style !== 'none' && (float) $outline_width > 0; + $has_shadow = $box_shadow !== 'none' && $box_shadow !== ''; + $is_visible = $has_outline || $has_shadow; + + if (!$is_inverted && !$is_visible) { + throw new ExpectationException(sprintf('Expected element "%s" to have a visible focus outline, but outline-style is "%s", outline-width is "%s", box-shadow is "%s".', $selector, $outline_style, $outline_width, $box_shadow), $this->getSession()->getDriver()); + } + + if ($is_inverted && $is_visible) { + throw new ExpectationException(sprintf('Expected element "%s" to not have a visible focus outline, but outline-style is "%s", outline-width is "%s", box-shadow is "%s".', $selector, $outline_style, $outline_width, $box_shadow), $this->getSession()->getDriver()); + } + } + /** * Assert that element with specified CSS is visible on page. * diff --git a/tests/behat/features/element.feature b/tests/behat/features/element.feature index 65e06973..42cead6f 100644 --- a/tests/behat/features/element.feature +++ b/tests/behat/features/element.feature @@ -497,6 +497,134 @@ Feature: Check that ElementTrait works Element matching css "#nonexistent-element" not found. """ + @javascript + Scenario: Assert "Then the element :selector should have keyboard focus" and its negative form work as expected + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + And I focus on the element "#focus-input" + Then the element "#focus-input" should have keyboard focus + And the element "#focus-button-outline" should not have keyboard focus + + @javascript + Scenario: Assert "Then the element :selector should have a visible focus outline" passes for an element with a CSS outline + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#focus-button-outline" should have a visible focus outline + And the element "#focus-button-no-outline" should not have a visible focus outline + + @javascript + Scenario: Assert "Then the element :selector should have a visible focus outline" passes for an element using box-shadow as the indicator + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#focus-button-shadow" should have a visible focus outline + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should have keyboard focus" fails when the element does not exist + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#nonexistent-element" should have keyboard focus + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Element matching css "#nonexistent-element" not found. + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should have keyboard focus" fails when a different element is focused + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + And I focus on the element "#focus-input" + Then the element "#focus-button-outline" should have keyboard focus + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Expected element "#focus-button-outline" to have keyboard focus, but focus is on: + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should have keyboard focus" fails when no element is focused + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#focus-input" should have keyboard focus + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Expected element "#focus-input" to have keyboard focus, but no element is focused. + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should not have keyboard focus" fails when the element is focused + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + And I focus on the element "#focus-input" + Then the element "#focus-input" should not have keyboard focus + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Expected element "#focus-input" to not have keyboard focus, but it does. + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should have a visible focus outline" fails when the element does not exist + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#nonexistent-element" should have a visible focus outline + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Element matching css "#nonexistent-element" not found. + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should have a visible focus outline" fails when the element has no visible indicator + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#focus-button-no-outline" should have a visible focus outline + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Expected element "#focus-button-no-outline" to have a visible focus outline, but outline-style is "none" + """ + + @trait:ElementTrait + Scenario: Assert "Then the element :selector should not have a visible focus outline" fails when the element has an outline + Given some behat configuration + And scenario steps tagged with "@javascript @phpserver": + """ + Given I am an anonymous user + When I visit "/sites/default/files/elements.html" + Then the element "#focus-button-outline" should not have a visible focus outline + """ + When I run "behat --no-colors" + Then it should fail with an error: + """ + Expected element "#focus-button-outline" to not have a visible focus outline, but outline-style is "solid" + """ + @javascript @phpserver Scenario: Assert click on element works Given I am on the phpserver test page diff --git a/tests/behat/fixtures/elements.html b/tests/behat/fixtures/elements.html index 818aa1c0..9a8b89eb 100644 --- a/tests/behat/fixtures/elements.html +++ b/tests/behat/fixtures/elements.html @@ -70,6 +70,9 @@

Navigation

Focus Testing

+ + +