Problem/Motivation

In working on Media Library, we paid a lot of attention to accessibility issues and fixed many problems in that space. We also tested our fixes. Many of the problems (and fixes) were related to where the focus goes while the user interacts with the UI. To facilitate more accessibility testing in JavaScript tests, it would be great if we had a way to assert that a particular element has, or does not have, focus.

Proposed resolution

Add two methods to JsWebAssert: assertElementHasFocus() and assertElementNotHasFocus().

Remaining tasks

Implement it, review it, commit it, and start using it!

User interface changes

None.

API changes

JsWebAssert, and therefore all JavaScript tests, will receive new assertion methods.

Data model changes

None.

Release notes snippet

TBD

Issue fork drupal-3041768

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

phenaproxima created an issue. See original summary.

phenaproxima’s picture

Title: Add a focus assertions to JavaScript tests » Add focus assertions to JavaScript tests
Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new2.44 KB

A first thing for interested parties to look at. Still needs explicit tests.

Rather than using jQuery, which is not loaded by default on all pages, this uses the document.activeElement property, which looks to have wide browser support: https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/ac...

wim leers’s picture

Totally makes sense to me! +1 for concept. And thanks for doing this! 🙏

  1. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +   * Tests that an element has focus.
    ...
    +   * Tests that an element does not have focus.
    

    s/Tests that/Asserts if/

  2. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +   *   The element selector type (CSS, XPath).
    

    \Behat\Mink\WebAssert::elementExists lists other possible values. Or is this case-insensitive? :O

  3. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +  public function assertHasFocus($selector_type, $selector, $message = 'Element has focus.') {
    

    It'd be better to set NULL as the default, so that we can detect if (is_null($message)) {…} and then generate a more meaningful error message, one that also conveys the used selector. This results in a better DX.

  4. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +   * Check the focus of a node.
    

    Must be third person singular.

  5. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +  private function checkFocus(NodeElement $node) {
    

    I wonder if elementHasFocus() would be more appropriate?

  6. +++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
    @@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
    +return document.evaluate("$xpath", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue === document.activeElement;
    

    Woah, what?!

    Could use docs.

seanb’s picture

+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -275,6 +275,66 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
+  public function assertHasFocus($selector_type, $selector, $message = 'Element has focus.') {
...
+  public function assertNotHasFocus($selector_type, $selector, $message = 'Element does not have focus.') {

This looks super useful! Can we add a container parameter to these methods? A selector could not be unique in the page, while it is unique in a container.

phenaproxima’s picture

StatusFileSize
new3.49 KB
new3.97 KB

#3:

  1. "Asserts if" seemed weird, and not in line with the other assertion methods. Changed it to "Asserts that".
  2. Made them lowercase, in line with \Behat\Mink\WebAssert::elementExists().
  3. I decided not to change this, since what we have now is in line with other methods of JsWebAssert.
  4. Fixed.
  5. Changed to hasFocus(), since I thought that elementHasFocused() sounded a bit too much like other assertion methods (elementExists() and company).
  6. Added a comment, with a URL to reference.

Also addressed #4.

lendude’s picture

+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -275,6 +276,76 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
+  public function assertHasFocus($selector_type, $selector, ElementInterface $container = NULL, $message = 'Element has focus.') {
...
+  public function assertNotHasFocus($selector_type, $selector, ElementInterface $container = NULL, $message = 'Element does not have focus.') {

Since the message being set here is the message in the exception when the assert fails, I would expect the messages to be the other way around

phenaproxima’s picture

StatusFileSize
new3.49 KB
new1.32 KB

D'oh! Good point. Fixed!

krzysztof domański’s picture

StatusFileSize
new2.51 KB
new3.75 KB

1. Improving PHPDoc commens.

2. Most methods of the WebAssert class do return the found elements or there is a plan to do it. Let's do the same in this case.
#2913411: Make methods in WebAssert return matching elements.

krzysztof domański’s picture

StatusFileSize
new1.17 KB
new3.75 KB

Re #3.2

vendor/behat/mink/src/Exception/ElementNotFoundException.php using lowercase element selector type so this is case sensitive.

class ElementNotFoundException extends ExpectationException
{
    /**
     * Initializes exception.
     *
     * @param DriverInterface|Session $driver   driver instance
     * @param string                  $type     element type
     * @param string                  $selector element selector type
     * @param string                  $locator  element locator
     */
    public function __construct($driver, $type = null, $selector = null, $locator = null)
    {
        $message = '';

        if (null !== $type) {
            $message .= ucfirst($type);
        } else {
            $message .= 'Tag';
        }

        if (null !== $locator) {
            if (null === $selector || in_array($selector, array('css', 'xpath'))) {
                $selector = 'matching '.($selector ?: 'locator');
            } else {
                $selector = 'with '.$selector;
            }
            $message .= ' '.$selector.' "'.$locator.'"';
        }

        $message .= ' not found.';

        parent::__construct($message, $driver);
    }
}
/**
 * Checks that specific element exists on the current page.
 *
 * @param string           $selectorType element selector type (css, xpath)
 * @param string|array     $selector     element selector
 * @param ElementInterface $container    document to check against
 *
 * @return NodeElement
 *
 * @throws ElementNotFoundException
 */
public function elementExists($selectorType, $selector, ElementInterface $container = null)
{
    $container = $container ?: $this->session->getPage();
    $node = $container->find($selectorType, $selector);

    if (null === $node) {
        if (is_array($selector)) {
            $selector = implode(' ', $selector);
        }

        throw new ElementNotFoundException($this->session->getDriver(), 'element', $selectorType, $selector);
    }

    return $node;
}
lauriii’s picture

+1 to adding assertions for this! 🔥

+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -275,6 +276,88 @@ public function assertNotVisibleInViewport($selector_type, $selector, $corner =
+return document.evaluate("$xpath", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue === document.activeElement;

To avoid false positives, it makes sense that we assert whether the specific element is focused, not taking into account that children could be focused. We should document this explicitly in the doc block to avoid confusion.

krzysztof domański’s picture

Follow-up #3.2 and #9 I created quick fix.

#3041900: The element selector type "CSS, XPath" in JSWebAssert should be lowercase

--- a/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/JSWebAssert.php
@@ -200,7 +200,7 @@ public function waitOnAutocomplete() {
    * @param string $selector_type
-   *   The element selector type (CSS, XPath).
+   *   The element selector type (css, xpath).
    * @param string|array $selector
@@ -243,7 +243,7 @@ public function assertVisibleInViewport($selector_type, $selector, $corner = FAL
    * @param string $selector_type
-   *   The element selector type (CSS, XPath).
+   *   The element selector type (css, xpath).
    * @param string|array $selector

It can be confusing so we should change it in other places.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Needs review » Needs work

+1 for this!

Sounds like there are small changes needed for #10 possibly #11.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mgifford’s picture

Another approach for this is described here https://opensource.com/article/23/2/automated-accessibility-testing

by using https://github.com/dmtrKovalenko/cypress-real-events

Not sure if there is a similar library for Nightwatch or if JsWebAssert is essentially equivalent.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.