Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions STEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,62 @@ Then the element "#content" should be centered in the viewport

</details>

<details>
<summary><code>@Then the element :selector should have keyboard focus</code></summary>

<br/>
Assert that the element has keyboard focus
<br/><br/>

```gherkin
Then the element "#edit-name" should have keyboard focus

```

</details>

<details>
<summary><code>@Then the element :selector should not have keyboard focus</code></summary>

<br/>
Assert that the element does not have keyboard focus
<br/><br/>

```gherkin
Then the element "#edit-name" should not have keyboard focus

```

</details>

<details>
<summary><code>@Then the element :selector should have a visible focus outline</code></summary>

<br/>
Assert that the element has a visible focus indicator
<br/><br/>

```gherkin
Then the element "#edit-name" should have a visible focus outline

```

</details>

<details>
<summary><code>@Then the element :selector should not have a visible focus outline</code></summary>

<br/>
Assert that the element does not have a visible focus indicator
<br/><br/>

```gherkin
Then the element "#decorative-icon" should not have a visible focus outline

```

</details>

<details>
<summary><code>@Then the element :selector should be displayed</code></summary>

Expand Down
152 changes: 152 additions & 0 deletions src/ElementTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<<JS
if ({{ELEMENT}} === document.activeElement) {
return '__OK__';
}
if (!document.activeElement || document.activeElement === document.body) {
return '__NONE__';
}
return document.activeElement.outerHTML.substring(0, 200);
JS;
$result = (string) $this->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 = <<<JS
var s = window.getComputedStyle({{ELEMENT}});
return s.outlineStyle + '|' + s.outlineWidth + '|' + s.boxShadow;
JS;
$result = (string) $this->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.
*
Expand Down
128 changes: 128 additions & 0 deletions tests/behat/features/element.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions tests/behat/fixtures/elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ <h2>Navigation</h2>
<div class="section">
<h2>Focus Testing</h2>
<input id="focus-input" type="text" placeholder="Focus me" onfocus="this.setAttribute('data-focused', 'true')">
<button id="focus-button-outline" type="button" style="outline: 2px solid blue; box-shadow: none;">Outlined button</button>
<button id="focus-button-shadow" type="button" style="outline: none; box-shadow: 0 0 0 2px rgb(0, 0, 255);">Shadow button</button>
<button id="focus-button-no-outline" type="button" style="outline: none; box-shadow: none;">No outline button</button>
</div>

<!-- Section: Hover Testing -->
Expand Down