From 347e042c411ef639d94e1c6fd1d25e7cbe39d90a Mon Sep 17 00:00:00 2001
From: bnjmnm <benm@umich.edu>
Date: Thu, 17 Feb 2022 10:25:13 -0500
Subject: [PATCH] Issue #3255809 by nod_, lauriii, xjm, hooroomoo: Add
 nightwatch tests for toolbar

---
 .../src/Nightwatch/Tests/toolbarApiTest.js    | 264 ++++++++++++
 .../tests/src/Nightwatch/Tests/toolbarTest.js | 379 ++++++++++++++++++
 core/scripts/dev/commit-code-check.sh         |   2 +-
 .../Commands/drupalInstallModule.js           |  26 ++
 4 files changed, 670 insertions(+), 1 deletion(-)
 create mode 100644 core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarApiTest.js
 create mode 100644 core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
 create mode 100644 core/tests/Drupal/Nightwatch/Commands/drupalInstallModule.js

diff --git a/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarApiTest.js b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarApiTest.js
new file mode 100644
index 000000000000..f35b24fb6c57
--- /dev/null
+++ b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarApiTest.js
@@ -0,0 +1,264 @@
+/**
+ * @file
+ * Tests of the existing Toolbar JS Api.
+ */
+
+module.exports = {
+  '@tags': ['core'],
+  before(browser) {
+    browser
+      .drupalInstall()
+      .drupalInstallModule('breakpoint')
+      .drupalInstallModule('toolbar')
+      .drupalCreateUser({
+        name: 'user',
+        password: '123',
+        permissions: [
+          'access site reports',
+          'access toolbar',
+          'administer menu',
+          'administer modules',
+          'administer site configuration',
+          'administer account settings',
+          'administer software updates',
+          'access content',
+          'administer permissions',
+          'administer users',
+        ],
+      })
+      .drupalLogin({ name: 'user', password: '123' })
+      .drupalRelativeURL('/')
+      .waitForElementPresent('#toolbar-administration', 10000);
+  },
+  beforeEach(browser) {
+    // Set the resolution to the default desktop resolution. Ensure the default
+    // toolbar is horizontal in headless mode.
+    browser.resizeWindow(1920, 1080);
+    // To clear active tab/tray from previous tests
+    browser.execute(function () {
+      localStorage.clear();
+      // Clear escapeAdmin url values.
+      sessionStorage.clear();
+    });
+    browser.drupalRelativeURL('/');
+  },
+  after(browser) {
+    browser.drupalUninstall();
+  },
+  'Drupal.Toolbar.models': (browser) => {
+    browser.execute(
+      function () {
+        const toReturn = {};
+        const { models } = Drupal.toolbar;
+        toReturn.hasMenuModel = models.hasOwnProperty('menuModel');
+        toReturn.menuModelType = typeof models.menuModel === 'object';
+        toReturn.hasToolbarModel = models.hasOwnProperty('toolbarModel');
+        toReturn.toolbarModelType = typeof models.toolbarModel === 'object';
+        toReturn.toolbarModelActiveTab =
+          models.toolbarModel.get('activeTab').id ===
+          'toolbar-item-administration';
+        toReturn.toolbarModelActiveTray =
+          models.toolbarModel.get('activeTray').id ===
+          'toolbar-item-administration-tray';
+        toReturn.toolbarModelIsOriented =
+          models.toolbarModel.get('isOriented') === true;
+        toReturn.toolbarModelIsFixed =
+          models.toolbarModel.get('isFixed') === true;
+        toReturn.toolbarModelAreSubtreesLoaded =
+          models.toolbarModel.get('areSubtreesLoaded') === false;
+        toReturn.toolbarModelIsViewportOverflowConstrained =
+          models.toolbarModel.get('isViewportOverflowConstrained') === false;
+        toReturn.toolbarModelOrientation =
+          models.toolbarModel.get('orientation') === 'horizontal';
+        toReturn.toolbarModelLocked =
+          models.toolbarModel.get('locked') === null;
+        toReturn.toolbarModelIsTrayToggleVisible =
+          models.toolbarModel.get('isTrayToggleVisible') === true;
+        toReturn.toolbarModelHeight = models.toolbarModel.get('height') === 79;
+        toReturn.toolbarModelOffsetsBottom =
+          models.toolbarModel.get('offsets').bottom === 0;
+        toReturn.toolbarModelOffsetsLeft =
+          models.toolbarModel.get('offsets').left === 0;
+        toReturn.toolbarModelOffsetsRight =
+          models.toolbarModel.get('offsets').right === 0;
+        toReturn.toolbarModelOffsetsTop =
+          models.toolbarModel.get('offsets').top === 79;
+        toReturn.toolbarModelSubtrees =
+          Object.keys(models.menuModel.get('subtrees')).length === 0;
+        return toReturn;
+      },
+      [],
+      (result) => {
+        const expectedTrue = {
+          hasMenuModel: 'has menu model',
+          menuModelType: 'menu model is an object',
+          hasToolbarModel: 'has toolbar model',
+          toolbarModelType: 'toolbar model is an object',
+          toolbarModelActiveTab: 'get("activeTab") has expected result',
+          toolbarModelActiveTray: 'get("activeTray") has expected result',
+          toolbarModelIsOriented: 'get("isOriented") has expected result',
+          toolbarModelIsFixed: 'get("isFixed") has expected result',
+          toolbarModelAreSubtreesLoaded:
+            'get("areSubtreesLoaded") has expected result',
+          toolbarModelIsViewportOverflowConstrained:
+            'get("isViewportOverflowConstrained") has expected result',
+          toolbarModelOrientation: 'get("orientation") has expected result',
+          toolbarModelLocked: 'get("locked") has expected result',
+          toolbarModelIsTrayToggleVisible:
+            'get("isTrayToggleVisible") has expected result',
+          toolbarModelHeight: 'get("height") has expected result',
+          toolbarModelOffsetsBottom:
+            'get("offsets") bottom has expected result',
+          toolbarModelOffsetsLeft: 'get("offsets") left has expected result',
+          toolbarModelOffsetsRight: 'get("offsets") right has expected result',
+          toolbarModelOffsetsTop: 'get("offsets") top has expected result',
+          toolbarModelSubtrees: 'get("subtrees") has expected result',
+        };
+        browser.assert.deepEqual(
+          Object.keys(expectedTrue).sort(),
+          Object.keys(result.value).sort(),
+          'Keys to check match',
+        );
+        Object.keys(expectedTrue).forEach((property) => {
+          browser.assert.equal(
+            result.value[property],
+            true,
+            expectedTrue[property],
+          );
+        });
+      },
+    );
+  },
+  'Change tab': (browser) => {
+    browser.execute(
+      function () {
+        const toReturn = {};
+        const { models } = Drupal.toolbar;
+        toReturn.hasMenuModel = models.hasOwnProperty('menuModel');
+        toReturn.menuModelType = typeof models.menuModel === 'object';
+        toReturn.hasToolbarModel = models.hasOwnProperty('toolbarModel');
+        toReturn.toolbarModelType = typeof models.toolbarModel === 'object';
+
+        const tab = document.querySelector('#toolbar-item-user');
+        tab.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+
+        toReturn.toolbarModelChangedTab =
+          models.toolbarModel.get('activeTab').id === 'toolbar-item-user';
+        toReturn.toolbarModelChangedTray =
+          models.toolbarModel.get('activeTray').id === 'toolbar-item-user-tray';
+        return toReturn;
+      },
+      [],
+      (result) => {
+        const expectedTrue = {
+          hasMenuModel: 'has menu model',
+          menuModelType: 'menu model is an object',
+          hasToolbarModel: 'has toolbar model',
+          toolbarModelType: 'toolbar model is an object',
+          toolbarModelChangedTab: 'get("activeTab") has expected result',
+          toolbarModelChangedTray: 'get("activeTray") has expected result',
+        };
+        browser.assert.deepEqual(
+          Object.keys(expectedTrue).sort(),
+          Object.keys(result.value).sort(),
+          'Keys to check match',
+        );
+        Object.keys(expectedTrue).forEach((property) => {
+          browser.assert.equal(
+            result.value[property],
+            true,
+            expectedTrue[property],
+          );
+        });
+      },
+    );
+  },
+  'Change orientation': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        const toReturn = {};
+        const { models } = Drupal.toolbar;
+
+        const orientationToggle = document.querySelector(
+          '#toolbar-item-administration-tray .toolbar-toggle-orientation button',
+        );
+        toReturn.toolbarOrientation =
+          models.toolbarModel.get('orientation') === 'horizontal';
+        orientationToggle.dispatchEvent(
+          new MouseEvent('click', { bubbles: true }),
+        );
+        setTimeout(() => {
+          toReturn.toolbarChangeOrientation =
+            models.toolbarModel.get('orientation') === 'vertical';
+          done(toReturn);
+        }, 100);
+      },
+      [],
+      (result) => {
+        const expectedTrue = {
+          toolbarOrientation: 'get("orientation") has expected result',
+          toolbarChangeOrientation: 'changing orientation has expected result',
+        };
+        browser.assert.deepEqual(
+          Object.keys(expectedTrue).sort(),
+          Object.keys(result.value).sort(),
+          'Keys to check match',
+        );
+        Object.keys(expectedTrue).forEach((property) => {
+          browser.assert.equal(
+            result.value[property],
+            true,
+            expectedTrue[property],
+          );
+        });
+      },
+    );
+  },
+  'Open submenu': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        const toReturn = {};
+        const { models } = Drupal.toolbar;
+        Drupal.toolbar.models.toolbarModel.set('orientation', 'vertical');
+        toReturn.toolbarOrientation =
+          models.toolbarModel.get('orientation') === 'vertical';
+        const manageTab = document.querySelector(
+          '#toolbar-item-administration',
+        );
+        Drupal.toolbar.models.toolbarModel.set('activeTab', manageTab);
+        const menuDropdown = document.querySelector(
+          '#toolbar-item-administration-tray button',
+        );
+        menuDropdown.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+
+        setTimeout(() => {
+          const statReportElement = document.querySelector(
+            '#toolbar-link-system-status',
+          );
+          toReturn.submenuItem =
+            statReportElement.textContent === 'Status report';
+          done(toReturn);
+        }, 100);
+      },
+      [],
+      (result) => {
+        const expectedTrue = {
+          toolbarOrientation: 'get("orientation") has expected result',
+          submenuItem: 'opening submenu has expected result',
+        };
+        browser.assert.deepEqual(
+          Object.keys(expectedTrue).sort(),
+          Object.keys(result.value).sort(),
+          'Keys to check match',
+        );
+        Object.keys(expectedTrue).forEach((property) => {
+          browser.assert.equal(
+            result.value[property],
+            true,
+            expectedTrue[property],
+          );
+        });
+      },
+    );
+  },
+};
diff --git a/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
new file mode 100644
index 000000000000..26689ec8a451
--- /dev/null
+++ b/core/modules/toolbar/tests/src/Nightwatch/Tests/toolbarTest.js
@@ -0,0 +1,379 @@
+/**
+ * @file
+ * Test the expected toolbar functionality.
+ */
+
+const itemAdministration = '#toolbar-item-administration';
+const itemAdministrationTray = '#toolbar-item-administration-tray';
+const adminOrientationButton = `${itemAdministrationTray} .toolbar-toggle-orientation button`;
+const itemUser = '#toolbar-item-user';
+const itemUserTray = '#toolbar-item-user-tray';
+const userOrientationBtn = `${itemUserTray} .toolbar-toggle-orientation button`;
+
+module.exports = {
+  '@tags': ['core'],
+  before(browser) {
+    browser
+      .drupalInstall()
+      .drupalInstallModule('breakpoint')
+      .drupalInstallModule('toolbar')
+      .drupalCreateUser({
+        name: 'user',
+        password: '123',
+        permissions: [
+          'access site reports',
+          'access toolbar',
+          'access administration pages',
+          'administer menu',
+          'administer modules',
+          'administer site configuration',
+          'administer account settings',
+          'administer software updates',
+          'access content',
+          'administer permissions',
+          'administer users',
+        ],
+      })
+      .drupalLogin({ name: 'user', password: '123' })
+      .drupalRelativeURL('/')
+      .waitForElementPresent('#toolbar-administration', 10000);
+  },
+  beforeEach(browser) {
+    browser.resizeWindow(1920, 1080);
+    browser.execute(function () {
+      // To clear active tab/tray from previous tests.
+      localStorage.clear();
+      // Clear escapeAdmin URL values.
+      sessionStorage.clear();
+    });
+    browser.drupalRelativeURL('/');
+  },
+  after(browser) {
+    browser.drupalUninstall();
+  },
+  'Change tab': (browser) => {
+    browser.waitForElementPresent(itemUserTray);
+    browser.assert.not.cssClassPresent(itemUser, 'is-active');
+    browser.assert.not.cssClassPresent(itemUserTray, 'is-active');
+    browser.click(itemUser);
+    browser.assert.cssClassPresent(itemUser, 'is-active');
+    browser.assert.cssClassPresent(itemUserTray, 'is-active');
+  },
+  'Change orientation': (browser) => {
+    browser.waitForElementPresent(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-horizontal',
+    );
+    browser.click(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+    browser.click(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-horizontal',
+    );
+  },
+  'Toggle tray': (browser) => {
+    browser.waitForElementPresent(itemUserTray);
+    browser.click(itemUser);
+    browser.assert.cssClassPresent(itemUserTray, 'is-active');
+    browser.click(itemUser);
+    browser.assert.not.cssClassPresent(itemUserTray, 'is-active');
+    browser.click(itemUser);
+    browser.assert.cssClassPresent(itemUserTray, 'is-active');
+  },
+  'Toggle submenu and sub-submenu': (browser) => {
+    browser.waitForElementPresent(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-horizontal',
+    );
+    browser.click(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+    browser.waitForElementPresent(
+      '#toolbar-item-administration-tray li:nth-child(4) button',
+    );
+    browser.assert.not.cssClassPresent(
+      '#toolbar-item-administration-tray li:nth-child(4)',
+      'open',
+    );
+    browser.assert.not.cssClassPresent(
+      '#toolbar-item-administration-tray li:nth-child(4) button',
+      'open',
+    );
+    browser.click('#toolbar-item-administration-tray li:nth-child(4) button');
+    browser.assert.cssClassPresent(
+      '#toolbar-item-administration-tray li:nth-child(4)',
+      'open',
+    );
+    browser.assert.cssClassPresent(
+      '#toolbar-item-administration-tray li:nth-child(4) button',
+      'open',
+    );
+    browser.expect
+      .element('#toolbar-link-user-admin_index')
+      .text.to.equal('People');
+    browser.expect
+      .element('#toolbar-link-system-admin_config_system')
+      .text.to.equal('System');
+    // Check sub-submenu.
+    browser.waitForElementPresent(
+      '#toolbar-item-administration-tray li.menu-item.level-2',
+    );
+    browser.assert.not.cssClassPresent(
+      '#toolbar-item-administration-tray li.menu-item.level-2',
+      'open',
+    );
+    browser.assert.not.cssClassPresent(
+      '#toolbar-item-administration-tray li.menu-item.level-2 button',
+      'open',
+    );
+    browser.click(
+      '#toolbar-item-administration-tray li.menu-item.level-2 button',
+    );
+    browser.assert.cssClassPresent(
+      '#toolbar-item-administration-tray li.menu-item.level-2',
+      'open',
+    );
+    browser.assert.cssClassPresent(
+      '#toolbar-item-administration-tray li.menu-item.level-2 button',
+      'open',
+    );
+    browser.expect
+      .element('#toolbar-link-entity-user-admin_form')
+      .text.to.equal('Account settings');
+  },
+  'Narrow toolbar width breakpoint': (browser) => {
+    browser.waitForElementPresent(adminOrientationButton);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-horizontal',
+    );
+    browser.assert.cssClassPresent(
+      '#toolbar-administration',
+      'toolbar-oriented',
+    );
+    browser.resizeWindow(263, 900);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+    browser.assert.not.cssClassPresent(itemAdministration, 'toolbar-oriented');
+  },
+  'Standard width toolbar breakpoint': (browser) => {
+    browser.resizeWindow(1000, 900);
+    browser.waitForElementPresent(adminOrientationButton);
+    browser.assert.cssClassPresent('body', 'toolbar-fixed');
+    browser.resizeWindow(609, 900);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+    browser.assert.not.cssClassPresent('body', 'toolbar-fixed');
+  },
+  'Wide toolbar breakpoint': (browser) => {
+    browser.waitForElementPresent(adminOrientationButton);
+    browser.resizeWindow(975, 900);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+  },
+  'Back to site link': (browser) => {
+    const escapeSelector = '[data-toolbar-escape-admin]';
+    browser.drupalRelativeURL('/user');
+    browser.drupalRelativeURL('/admin');
+    // Don't check the visibility as stark doesn't add the .path-admin class
+    // to the <body> required to display the button.
+    browser.assert.attributeContains(escapeSelector, 'href', '/user/2');
+  },
+  'Aural view test: tray orientation': (browser) => {
+    browser.waitForElementPresent(
+      '#toolbar-item-administration-tray .toolbar-toggle-orientation button',
+    );
+    browser.executeAsync(
+      function (done) {
+        Drupal.announce = done;
+
+        const orientationButton = document.querySelector(
+          '#toolbar-item-administration-tray .toolbar-toggle-orientation button',
+        );
+        orientationButton.dispatchEvent(
+          new MouseEvent('click', { bubbles: true }),
+        );
+      },
+      (result) => {
+        browser.assert.equal(
+          result.value,
+          'Tray orientation changed to vertical.',
+        );
+      },
+    );
+    browser.executeAsync(
+      function (done) {
+        Drupal.announce = done;
+
+        const orientationButton = document.querySelector(
+          '#toolbar-item-administration-tray .toolbar-toggle-orientation button',
+        );
+        orientationButton.dispatchEvent(
+          new MouseEvent('click', { bubbles: true }),
+        );
+      },
+      (result) => {
+        browser.assert.equal(
+          result.value,
+          'Tray orientation changed to horizontal.',
+        );
+      },
+    );
+  },
+  'Aural view test: tray toggle': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        Drupal.announce = done;
+        const $adminButton = jQuery('#toolbar-item-administration');
+        $adminButton.trigger('click');
+      },
+      (result) => {
+        browser.assert.equal(
+          result.value,
+          'Tray "Administration menu" closed.',
+        );
+      },
+    );
+    browser.executeAsync(
+      function (done) {
+        Drupal.announce = done;
+        const $adminButton = jQuery('#toolbar-item-administration');
+        $adminButton.trigger('click');
+      },
+      (result) => {
+        browser.assert.equal(
+          result.value,
+          'Tray "Administration menu" opened.',
+        );
+      },
+    );
+  },
+  'Toolbar event: drupalToolbarOrientationChange': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        jQuery(document).on(
+          'drupalToolbarOrientationChange',
+          function (event, orientation) {
+            done(orientation);
+          },
+        );
+        const orientationButton = document.querySelector(
+          '#toolbar-item-administration-tray .toolbar-toggle-orientation button',
+        );
+        orientationButton.dispatchEvent(
+          new MouseEvent('click', { bubbles: true }),
+        );
+      },
+      (result) => {
+        browser.assert.equal(result.value, 'vertical');
+      },
+    );
+  },
+  'Toolbar event: drupalToolbarTabChange': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        jQuery(document).on('drupalToolbarTabChange', function (event, tab) {
+          done(tab.id);
+        });
+        jQuery('#toolbar-item-user').trigger('click');
+      },
+      (result) => {
+        browser.assert.equal(result.value, 'toolbar-item-user');
+      },
+    );
+  },
+  'Toolbar event: drupalToolbarTrayChange': (browser) => {
+    browser.executeAsync(
+      function (done) {
+        const $adminButton = jQuery('#toolbar-item-administration');
+        // Hide the admin menu first, this event is not firing reliably
+        // otherwise.
+        $adminButton.trigger('click');
+        jQuery(document).on('drupalToolbarTrayChange', function (event, tray) {
+          done(tray.id);
+        });
+        $adminButton.trigger('click');
+      },
+      (result) => {
+        browser.assert.equal(result.value, 'toolbar-item-administration-tray');
+      },
+    );
+  },
+  'Locked toolbar vertical wide viewport': (browser) => {
+    browser.resizeWindow(1000, 900);
+    browser.waitForElementPresent(adminOrientationButton);
+    // eslint-disable-next-line no-unused-expressions
+    browser.expect.element(adminOrientationButton).to.be.visible;
+    browser.resizeWindow(975, 900);
+    browser.assert.cssClassPresent(
+      itemAdministrationTray,
+      'is-active toolbar-tray-vertical',
+    );
+    // eslint-disable-next-line no-unused-expressions
+    browser.expect.element(adminOrientationButton).to.not.be.visible;
+  },
+  'Settings are retained on refresh': (browser) => {
+    browser.waitForElementPresent(itemUser);
+    // Set user as active tab.
+    browser.assert.not.cssClassPresent(itemUser, 'is-active');
+    browser.assert.not.cssClassPresent(itemUserTray, 'is-active');
+    browser.click(itemUser);
+    // Check tab and tray are open.
+    browser.assert.cssClassPresent(itemUser, 'is-active');
+    browser.assert.cssClassPresent(itemUserTray, 'is-active');
+    // Set orientation to vertical.
+    browser.waitForElementPresent(userOrientationBtn);
+    browser.assert.cssClassPresent(
+      itemUserTray,
+      'is-active toolbar-tray-horizontal',
+    );
+    browser.click(userOrientationBtn);
+    browser.assert.cssClassPresent(
+      itemUserTray,
+      'is-active toolbar-tray-vertical',
+    );
+    browser.refresh();
+    // Check user tab is active.
+    browser.assert.cssClassPresent(itemUser, 'is-active');
+    // Check tray is active and orientation is vertical.
+    browser.assert.cssClassPresent(
+      itemUserTray,
+      'is-active toolbar-tray-vertical',
+    );
+  },
+  'Check toolbar overlap with page content': (browser) => {
+    browser.assert.cssClassPresent('body', 'toolbar-horizontal');
+    browser.execute(
+      () => {
+        const toolbar = document.querySelector('#toolbar-administration');
+        const nextElement = toolbar.nextElementSibling.getBoundingClientRect();
+        const tray = document
+          .querySelector('#toolbar-item-administration-tray')
+          .getBoundingClientRect();
+        // Page content should start after the toolbar height to not overlap.
+        return nextElement.top > tray.top + tray.height;
+      },
+      (result) => {
+        browser.assert.equal(
+          result.value,
+          true,
+          'Toolbar and page content do not overlap',
+        );
+      },
+    );
+  },
+};
diff --git a/core/scripts/dev/commit-code-check.sh b/core/scripts/dev/commit-code-check.sh
index 08e613228c8e..e829dd31529e 100755
--- a/core/scripts/dev/commit-code-check.sh
+++ b/core/scripts/dev/commit-code-check.sh
@@ -353,7 +353,7 @@
   ############################################################################
   ### JAVASCRIPT FILES
   ############################################################################
-  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ ! $FILE =~ ^core/tests/Drupal/Nightwatch ]] && [[ ! $FILE =~ ^core/assets/vendor/jquery.ui/ui ]] && [[ ! $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
+  if [[ -f "$TOP_LEVEL/$FILE" ]] && [[ $FILE =~ \.js$ ]] && [[ ! $FILE =~ ^core/tests/Drupal/Nightwatch ]] && [[ ! $FILE =~ /tests/src/Nightwatch/ ]] && [[ ! $FILE =~ ^core/assets/vendor/jquery.ui/ui ]] && [[ ! $FILE =~ ^core/modules/ckeditor5/js/ckeditor5_plugins ]]; then
     # Work out the root name of the JavaScript so we can ensure that the ES6
     # version has been compiled correctly.
     if [[ $FILE =~ \.es6\.js$ ]]; then
diff --git a/core/tests/Drupal/Nightwatch/Commands/drupalInstallModule.js b/core/tests/Drupal/Nightwatch/Commands/drupalInstallModule.js
new file mode 100644
index 000000000000..4e20ac7f1f91
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Commands/drupalInstallModule.js
@@ -0,0 +1,26 @@
+/**
+ * Install the given module.
+ *
+ * @param {string} module
+ *   The module machine name to enable.
+ * @param {function} callback
+ *   A callback which will be called, when the module has been enabled.
+ * @return {object}
+ *   The drupalInstallModule command.
+ */
+exports.command = function drupalInstallModule(module, callback) {
+  const self = this;
+  this.drupalLoginAsAdmin(() => {
+    this.drupalRelativeURL('/admin/modules')
+      .click(`input[data-drupal-selector="edit-modules-${module}-enable"]`)
+      .click('input[data-drupal-selector="edit-submit"]')
+      // Wait for the install message to show up.
+      .waitForElementVisible('.system-modules', 10000);
+  }).perform(() => {
+    if (typeof callback === 'function') {
+      callback.call(self);
+    }
+  });
+
+  return this;
+};
-- 
GitLab