Repository: nightwatchjs/nightwatch (js)

Issue:
`suppressNotFoundErrors` in new Element API suppress other errors as well.
### Description of the bug/issue

`suppressNotFoundErrors` property in the new Element API should ideally only suppress the `NoSuchElementError` but instead it suppresses all the errors that the WebDriver encounters and returns while trying to find the element.

For example, it also suppresses the `InvalidSelectorError`, which is thrown when the selector passed to the `find()` command is invalid, which could be a human error while writing the test.

### Steps to reproduce

In any test, run the following command:
```js
await browser.element.find({selector: '@something', suppressNotFoundErrors: true});
```

The above command will suppress and not throw any error even though the error is `InvalidSelectorError` and not `NoSuchElementError`.


### Sample test

_No response_

### Command to run

_No response_

### Verbose Output

_No response_

### Nightwatch Configuration

_No response_

### Nightwatch.js Version

latest

### Node Version

_No response_

### Browser

_No response_

### Operating System

_No response_

### Additional Information

_No response_

Pull request:
Fix #4223: Restrict suppressNotFoundErrors to NoSuchElementError
## Description
This pull request addresses issue #4223. The suppressNotFoundErrors property in the new Element API previously suppressed all errors encountered by the WebDriver, including errors like InvalidSelectorError. This behavior was unintended, as suppressNotFoundErrors should ideally only suppress NoSuchElementError.

## Changes Made

Updated scoped-element.js to catch and handle NoSuchElementError specifically when suppressNotFoundErrors is enabled.
Modified the logic to allow other errors, like InvalidSelectorError, to propagate as expected.

## Steps to Reproduce the Issue
Use an invalid selector in a test command:
await browser.element.find({
    selector: '@something',
    suppressNotFoundErrors: true
});
Observe that InvalidSelectorError is no longer suppressed and propagates as intended.

## Proof of Work

Terminal Output
The attached screenshot shows the updated terminal output, where an InvalidSelectorError is correctly thrown when an invalid selector is provided.
<img width="1064" alt="Screenshot 2024-11-09 at 1 04 26 AM" src="https://github.com/user-attachments/assets/c30dba9a-6571-4988-b6c1-6f8770ddcce7">

Unit Test
A unit test was added to ecosia.js to validate this behavior. The test checks that an invalid selector throws InvalidSelectorError instead of suppressing it.
<img width="777" alt="Screenshot 2024-11-09 at 1 04 50 AM" src="https://github.com/user-attachments/assets/7836cf51-c5a6-4fa9-8cdd-aa98dbc24494">

Thanks in advance for your contribution. Please follow the below steps in submitting a pull request, as it will help us with reviewing it quicker.

- [x ] Before marking your PR for review, please test and verify your changes by making appropriate modifications to any of the Nightwatch example tests (present in `examples/tests` directory of the project) and running them. `ecosia.js` and `duckDuckGo.js` are good examples to work with.
- [ x] Create a new branch from master (e.g. `features/my-new-feature` or `issue/123-my-bugfix`);
- [x ] If you're fixing a bug also create an issue if one doesn't exist yet;
- [x ] If it's a new feature explain why do you think it's necessary. Please check with the maintainers beforehand to make sure it is something that we will accept. Usually we only accept new features if we feel that they will benefit the entire community;
- [x ] Please avoid sending PRs which contain drastic or low level changes. If you are certain that the changes are needed, please discuss them beforehand and indicate what the impact will be;
- [x ] If your change is based on existing functionality please consider refactoring first. Pull requests that duplicate code will most likely be ignored;
- [x] Do not include changes that are not related to the issue at hand;
- [x] Follow the same coding style with regards to spaces, semicolons, variable naming etc.;
- [x] Always add unit tests - PRs without tests are most of the times ignored.


Patch:
diff --git a/lib/api/_loaders/command.js b/lib/api/_loaders/command.js
index 3afd576d..da1ac057 100644
--- a/lib/api/_loaders/command.js
+++ b/lib/api/_loaders/command.js
@@ -183,7 +183,16 @@ class CommandLoader extends BaseCommandLoader {
           })
           .catch(err => {
             if (instance instanceof EventEmitter) {
-              instance.emit('error', err);
+              if (instance.needsPromise) {
+                // if the instance has `needsPromise` set to `true`, the `error` event is listened
+                // on the `context` object, not on the `instance` object (in `treenode.js`).
+                this.emit('error', err);
+              } else {
+                // for class-based commands that inherit from EventEmitter.
+                // Since the `needsPromise` is set to `false` in this case, the `complete` and `error`
+                // events are listened on the `instance` object.
+                instance.emit('error', err);
+              }
 
               return;
             }
diff --git a/lib/api/web-element/commands/getAccessibleName.js b/lib/api/web-element/commands/getAccessibleName.js
index ac531f1d..cf641333 100644
--- a/lib/api/web-element/commands/getAccessibleName.js
+++ b/lib/api/web-element/commands/getAccessibleName.js
@@ -20,9 +20,11 @@
  * @method getAccessibleName
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).getAccessibleName()
+ * @syntax browser.element.find(selector).getAccessibleName()
+ * @syntax browser.element.find(selector).getComputedLabel()
  * @link /#get-computed-label
  * @returns {ScopedValue<string>} A container with accessible name of an element.
+ * @alias getComputedLabel
  */
 module.exports.command = function() {
   return this.runQueuedCommandScoped('getElementAccessibleName');
diff --git a/lib/api/web-element/commands/getAriaRole.js b/lib/api/web-element/commands/getAriaRole.js
index 68ac1a78..b9c04cb2 100644
--- a/lib/api/web-element/commands/getAriaRole.js
+++ b/lib/api/web-element/commands/getAriaRole.js
@@ -19,9 +19,11 @@
  * @method getAriaRole
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).getAriaRole()
+ * @syntax browser.element.find(selector).getAriaRole()
+ * @syntax browser.element.find(selector).getComputedRole()
  * @link /#get-computed-role
  * @returns {ScopedValue<string>} The container with computed WAI-ARIA role of an element.
+ * @alias getComputedRole
  */
 module.exports.command = function() {
   return this.runQueuedCommandScoped('getElementAriaRole');
diff --git a/lib/api/web-element/commands/isActive.js b/lib/api/web-element/commands/isActive.js
new file mode 100644
index 00000000..9d756142
--- /dev/null
+++ b/lib/api/web-element/commands/isActive.js
@@ -0,0 +1,31 @@
+/**
+ * Determines if an element is currently active/focused in the DOM.
+ *
+ * For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
+ * For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
+ *
+ * @example
+ * describe('isActive Demo', function() {
+ *   it('test isActive', function(browser) {
+ *     browser.element.find('#search')
+ *       .isActive()
+ *       .assert.equals(true);
+ *   });
+ *
+ *   it('test async isActive', async function(browser) {
+ *     const result = await browser.element.find('#search').isActive();
+ *     browser.assert.equal(result, true);
+ *   });
+ * });
+ *
+ * @since 3.9.0
+ * @method isActive
+ * @memberof ScopedWebElement
+ * @instance
+ * @syntax browser.element.find(selector).isActive()
+ * @link /#get-active-element
+ * @returns {ScopedValue<boolean>}
+ */
+module.exports.command = function () {
+  return this.runQueuedCommandScoped('isElementActive');
+};
diff --git a/lib/api/web-element/commands/isEnabled.js b/lib/api/web-element/commands/isEnabled.js
index ac36a975..070b4e85 100644
--- a/lib/api/web-element/commands/isEnabled.js
+++ b/lib/api/web-element/commands/isEnabled.js
@@ -2,17 +2,18 @@
  * Determines if an element is enabled.
  *
  * For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
+ * For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
  *
  * @example
  * describe('isEnabled Demo', function() {
  *   it('test isEnabled', function(browser) {
- *     browser.element('#search')
+ *     browser.element.find('#search')
  *       .isEnabled()
  *       .assert.equals(true);
  *   });
  *
  *   it('test async isEnabled', async function(browser) {
- *     const result = await browser.element('#search').isEnabled();
+ *     const result = await browser.element.find('#search').isEnabled();
  *     browser.assert.equal(result, true);
  *   });
  * });
@@ -21,7 +22,7 @@
  * @method isEnabled
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).isEnabled()
+ * @syntax browser.element.find(selector).isEnabled()
  * @link /#is-element-enabled
  * @returns {ScopedValue<boolean>}
  */
diff --git a/lib/api/web-element/commands/isPresent.js b/lib/api/web-element/commands/isPresent.js
index 3a588ee6..be62d0f3 100644
--- a/lib/api/web-element/commands/isPresent.js
+++ b/lib/api/web-element/commands/isPresent.js
@@ -1,20 +1,21 @@
 /**
  * Checks if an element is present in the DOM.
  *
- * This command is useful for verifying the presence of elements that may not be visible or interactable.
+ * This command is useful for verifying the presence of elements that may/may not be visible or interactable.
  *
- * For more information on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/working-with-page-elements/finding-elements.html">Finding Elements</a> guide page.
+ * For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
+ * For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
  *
  * @example
  * describe('isPresent Demo', function() {
  *   it('test isPresent', function(browser) {
- *     browser.element('#search')
+ *     browser.element.find('#search')
  *       .isPresent()
  *       .assert.equals(true);
  *   });
  *
  *   it('test async isPresent', async function(browser) {
- *     const result = await browser.element('#search').isPresent();
+ *     const result = await browser.element.find('#search').isPresent();
  *     browser.assert.equal(result, true);
  *   });
  * });
@@ -23,7 +24,7 @@
  * @method isPresent
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).isPresent()
+ * @syntax browser.element.find(selector).isPresent()
  * @returns {ScopedValue<boolean>} A boolean value indicating if the element is present in the DOM.
  */
 
diff --git a/lib/api/web-element/commands/isSelected.js b/lib/api/web-element/commands/isSelected.js
index 075bd800..a20ec39b 100644
--- a/lib/api/web-element/commands/isSelected.js
+++ b/lib/api/web-element/commands/isSelected.js
@@ -2,17 +2,18 @@
  * Determines if an element is selected.
  *
  * For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
+ * For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
  *
  * @example
  * describe('isSelected Demo', function() {
  *   it('test isSelected', function(browser) {
- *     browser.element('#search')
+ *     browser.element.find('#search')
  *       .isSelected()
  *       .assert.equals(true);
  *   });
  *
  *   it('test async isSelected', async function(browser) {
- *     const result = await browser.element('#search').isSelected();
+ *     const result = await browser.element.find('#search').isSelected();
  *     browser.assert.equal(result, true);
  *   });
  * });
@@ -21,7 +22,7 @@
  * @method isSelected
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).isSelected()
+ * @syntax browser.element.find(selector).isSelected()
  * @link /#is-element-selected
  * @returns {ScopedValue<boolean>}
  */
diff --git a/lib/api/web-element/commands/isVisible.js b/lib/api/web-element/commands/isVisible.js
index fe5cbc19..e42654da 100644
--- a/lib/api/web-element/commands/isVisible.js
+++ b/lib/api/web-element/commands/isVisible.js
@@ -2,15 +2,18 @@
  * Determines if an element is visible.
  *
  * For more info on working with DOM elements in Nightwatch, refer to the <a href="https://nightwatchjs.org/guide/writing-tests/finding-interacting-with-dom-elements.html">Finding & interacting with DOM Elements</a> guide page.
+ * For more info on the new `browser.element.find()` syntax, refer to the <a href="/api/element/"> new Element API Overview </a> page.
  *
  *  @example
  * describe('isVisible demo', function() {
  *   it('test isVisible', function(browser) {
- *     browser.element('#search').isVisible().assert.equals(true);
+ *     browser.element.find('#search')
+ *       .isVisible()
+ *       .assert.equals(true);
  *   });
  *
  *   it('test async isVisible', async function(browser) {
- *     const result = await browser.element('#search').isVisible();
+ *     const result = await browser.element.find('#search').isVisible();
  *     browser.assert.equal(result, true);
  *   });
  * });
@@ -19,7 +22,7 @@
  * @method isVisible
  * @memberof ScopedWebElement
  * @instance
- * @syntax browser.element(selector).isVisible()
+ * @syntax browser.element.find(selector).isVisible()
  * @link /#element-displayedness
  * @returns {ScopedValue<boolean>}
  * @alias isDisplayed
diff --git a/lib/api/web-element/scoped-element.js b/lib/api/web-element/scoped-element.js
index 8dffc0b8..49c3a5b7 100644
--- a/lib/api/web-element/scoped-element.js
+++ b/lib/api/web-element/scoped-element.js
@@ -27,9 +27,9 @@ class ScopedWebElement {
       'getProperty': ['property', 'prop'],
       'getText': ['text'],
       'getTagName': ['tagName'],
-      'getAccessibleName': ['accessibleName'],
+      'getAccessibleName': ['accessibleName', 'getComputedLabel'],
       'getCssProperty': ['css', 'getCssValue'],
-      'getAriaRole': ['ariaRole'],
+      'getAriaRole': ['ariaRole', 'getComputedRole'],
       'isVisible': ['isDisplayed']
     };
   }
@@ -180,11 +180,14 @@ class ScopedWebElement {
 
         return await this.findElement({parentElement, selector: condition, index, timeout, retryInterval});
       } catch (error) {
-        if (this.suppressNotFoundErrors) {
+        const narrowedError = createNarrowedError({error, condition, timeout});
+        if (
+          this.suppressNotFoundErrors &&
+          narrowedError.name === 'NoSuchElementError'
+        ) {
           return null;
         }
 
-        const narrowedError = createNarrowedError({error, condition, timeout});
         Logger.error(narrowedError);
 
         if (abortOnFailure) {
@@ -356,6 +359,8 @@ function createNarrowedError({error, condition, timeout}) {
     return err;
   }
 
+  error.message = `Error occurred while trying to locate element "${condition}": ${error.message || 'unknown error'}`;
+
   return error;
 }
 
diff --git a/lib/core/treenode.js b/lib/core/treenode.js
index 1bfa4ae3..057cbb0f 100644
--- a/lib/core/treenode.js
+++ b/lib/core/treenode.js
@@ -206,6 +206,9 @@ class TreeNode {
       commandResult = this.instance;
       if (this.instance.needsPromise) {
         this.needsPromise = true;
+        // this change was done because the only way function-styled custom commands could be resolved
+        // is if they contain another NW API command, which could call `node.context.emit('complete')`
+        // inside `asynctree.js > resolveNode` method for the custom command node.
         commandResult = this.context;
       }
     } else if (this.context instanceof EventEmitter) { // Chai assertions
diff --git a/lib/transport/selenium-webdriver/method-mappings.js b/lib/transport/selenium-webdriver/method-mappings.js
index cde1de0e..709e85d4 100644
--- a/lib/transport/selenium-webdriver/method-mappings.js
+++ b/lib/transport/selenium-webdriver/method-mappings.js
@@ -493,6 +493,15 @@ module.exports = class MethodMappings {
           };
         },
 
+        async isElementActive(webElementOrId) {
+          const element = this.getWebElement(webElementOrId);
+          const elementId = await element.getId();
+
+          const currentActiveElementId = await this.methods.session.getActiveElement.call(this);
+
+          return elementId === currentActiveElementId;
+        },
+
         async setElementProperty(webElementOrId, name, value) {
           const element = this.getWebElement(webElementOrId);
 
diff --git a/package-lock.json b/package-lock.json
index e3f38aaf..1a8598c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "nightwatch",
-  "version": "3.8.1",
+  "version": "3.9.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "nightwatch",
-      "version": "3.8.1",
+      "version": "3.9.0",
       "license": "MIT",
       "dependencies": {
         "@nightwatch/chai": "5.0.3",
diff --git a/package.json b/package.json
index 7049bb2d..e9c2f0b1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "nightwatch",
   "description": "Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.",
-  "version": "3.8.1",
+  "version": "3.9.0",
   "author": "Andrei Rusu",
   "homepage": "https://nightwatchjs.org",
   "main": "./lib/index.js",
diff --git a/types/custom-command.d.ts b/types/custom-command.d.ts
index a9bb75e0..b36e14f6 100644
--- a/types/custom-command.d.ts
+++ b/types/custom-command.d.ts
@@ -1,3 +1,7 @@
+import {CommandInstance} from './index';
+
+export interface CustomCommandInstance extends CommandInstance {}
+
 export interface NightwatchCustomCommandsModel {
   /**
    * Define a custom command
@@ -5,14 +9,14 @@ export interface NightwatchCustomCommandsModel {
    * @example
    * class LogMessage implements NightwatchCustomCommandsModel {
    *   command() {
-   *     
+   *
    *     return Promise.resolve();
    *   }
    * }
    *
    * @see https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html#define-a-custom-command
    */
-  command: (...args: any) => unknown | Promise<unknown>;
+  command: (...args: any[]) => unknown | Promise<unknown>;
 }
 
 /**
diff --git a/types/index.d.ts b/types/index.d.ts
index 2d7254ec..ae650929 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -68,6 +68,10 @@ export type NightwatchGenericCallback<T> = (
 
 export type Awaitable<T, V> = Omit<T, 'then'> & PromiseLike<V>;
 
+export type KeysFilter<T, U> = {
+  [K in keyof T]-?: T[K] extends U ? K : never;
+}[keyof T];
+
 // tslint:disable-next-line
 type VoidToNull<T> = T extends void ? null : T;
 
@@ -1149,6 +1153,77 @@ export interface NightwatchClientObject {
   sessionId: string | null;
 }
 
+export interface HttpRequestOptions {
+  /**
+   * The pathname of the endpoint to call. Ex: `'/session/:sessionId/url'`.
+   *
+   * Alternatively, url property could be provided with the full URL.
+   */
+  path?: string;
+  data?: unknown;
+
+  /**
+   * For custom-commands, set to `this.api.sessionId`.
+   */
+  sessionId: string;
+
+  method: 'POST' | 'GET' | 'DELETE' | 'PUT';
+  use_ssl?: boolean;
+  host?: string;
+  port?: number;
+
+  /**
+   * The full URL to call. Ex: `http://localhost:4444/session/:sessionId/url`.
+   */
+  url?: string;
+
+  auth?: {
+    user: string;
+    pass: string;
+  }
+}
+
+export interface CommandInstance {
+  get api(): NightwatchAPI;
+  get client(): NightwatchClient;
+  get commandArgs(): unknown[];
+  get commandFileName(): string;
+  get driver(): WebDriver;
+  get isES6AsyncCommand(): boolean;
+  get reuseBrowser(): boolean;
+
+  /**
+   * Direct access to methods present in the `lib/transport/selenium-webdriver/method-mappings.js` file
+   * of Nightwatch code.
+   *
+   * TODO: complete the type definition.
+   *
+   * For now, you would need to create custom interface to use this property, like below:
+   * ```ts
+   * interface TransportActions {
+   *   getCurrentUrl(): Promise<NightwatchCallbackResult<string>>;
+   * }
+   * ```
+   * then use it inside your custom command like:
+   * ```ts
+   * const currentUrl = await (this.transportActions as TransportActions).getCurrentUrl();
+   * ```
+   */
+  get transportActions(): unknown;
+
+  /**
+   * Directly call the HTTP endpoints of the Selenium/WebDriver server.
+   *
+   * This is useful when you need to call a command that is not directly supported by Nightwatch API.
+   *
+   * @see https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html#postdoc-directly-calling-seleniumwebdriver-endpoints
+   */
+  httpRequest(options: HttpRequestOptions): Promise<unknown>;
+
+  toString(): string;
+  complete(...args: unknown[]): void;
+}
+
 export interface CreateClientParams {
   browserName?: string | null;
   headless?: boolean;
diff --git a/types/page-object.d.ts b/types/page-object.d.ts
index 1e663c13..3e15e155 100644
--- a/types/page-object.d.ts
+++ b/types/page-object.d.ts
@@ -9,8 +9,8 @@ import {
   ElementCommands,
   ElementFunction,
   Expect,
+  KeysFilter,
   LocateStrategy,
-  NamespacedApi,
   NightwatchAPI,
   NightwatchClient,
   NightwatchComponentTestingCommands,
@@ -147,6 +147,7 @@ export type EnhancedSectionInstance<
   Commands &
   ElementCommands &
   ChromiumClientCommands &
+  Pick<NightwatchCustomCommands, KeysFilter<NightwatchCustomCommands, Function>> & // eslint-disable-line @typescript-eslint/ban-types
   Pick<
     NightwatchComponentTestingCommands,
     'importScript' | 'launchComponentRenderer' | 'mountComponent'
@@ -507,7 +508,19 @@ export type EnhancedPageObject<
   > &
   Required<MergeObjectsArray<Commands>> & {
     /**
-     * A url or function returning a url to be used in a url() command when the page's navigate() method is called.
+     * A url or a function returning a url. This is also used internally when the page's navigate() method is called.
+     *
+     * By default, the url property is set to a string type. To set it to a function type instead, the fifth type parameter
+     * of the `EnhancedPageObject` interface can be used:
+     *
+     * ```
+     * export interface MyPage extends
+     *   EnhancedPageObject<{}, typeof elements, {}, {}, () => string> {}
+     * ```
+     *
+     * See the usage of the `EnhancedPageObject` interface
+     * [here](https://github.com/nightwatchjs-community/nightwatch-typescript-boilerplate/blob/0bf15a6e8735b576b82bce9a7bb5c6beddb14de4/nightwatch/pages/FileUpload.ts#L17)
+     * for reference.
      *
      * @example
      * const homePageObject = browser.page.homePage();
diff --git a/types/web-element.d.ts b/types/web-element.d.ts
index 42677806..6505b388 100644
--- a/types/web-element.d.ts
+++ b/types/web-element.d.ts
@@ -188,9 +188,11 @@ export interface ScopedElement extends Element, PromiseLike<WebElement> {
 
   getAccessibleName(): ElementValue<string>;
   accessibleName(): ElementValue<string>;
+  getComputedLabel(): ElementValue<string>;
 
   getAriaRole(): ElementValue<string>;
   ariaRole(): ElementValue<string>;
+  getComputedRole(): ElementValue<string>;
 
   getCssProperty(name: string): ElementValue<string>;
   css(name: string): ElementValue<string>;
@@ -216,6 +218,8 @@ export interface ScopedElement extends Element, PromiseLike<WebElement> {
 
   isVisible(): ElementValue<boolean>;
   isDisplayed(): ElementValue<boolean>;
+
+  isActive(): ElementValue<boolean>;
 }
 
 type WaitUntilOptions = {


Test patch:
diff --git a/test/sampletests/webelement/findWithSuppressNotFoundErrors.js b/test/sampletests/webelement/findWithSuppressNotFoundErrors.js
new file mode 100644
index 00000000..3f71876f
--- /dev/null
+++ b/test/sampletests/webelement/findWithSuppressNotFoundErrors.js
@@ -0,0 +1,8 @@
+describe('test', function () {
+  test('test find with suppressNotFoundErrors', async (browser) => {
+    browser.element.find({
+      selector: browser.globals.selector,
+      suppressNotFoundErrors: browser.globals.suppressNotFoundErrors
+    });
+  });
+});
diff --git a/test/src/api/commands/web-element/testFindOnErrors.js b/test/src/api/commands/web-element/testFindOnErrors.js
new file mode 100644
index 00000000..09810ed4
--- /dev/null
+++ b/test/src/api/commands/web-element/testFindOnErrors.js
@@ -0,0 +1,171 @@
+const assert = require('assert');
+const path = require('path');
+const MockServer  = require('../../../../lib/mockserver.js');
+const Mocks = require('../../../../lib/command-mocks.js');
+const CommandGlobals = require('../../../../lib/globals/commands-w3c.js');
+const common = require('../../../../common.js');
+const NightwatchClient = common.require('index.js');
+const {settings} = common;
+
+describe('element().isPresent() command', function() {
+  before(function (done) {
+    CommandGlobals.beforeEach.call(this, done);
+
+  });
+
+  after(function (done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: true) suppresses NoSuchElementError', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '#wrong'}),
+      response: JSON.stringify({
+        value: []
+      })
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        if (Object.prototype.hasOwnProperty.call(results, 'lastError')) {
+          assert.notStrictEqual(results.lastError.name, 'NoSuchElementError');
+        }
+      },
+      waitForConditionTimeout: 100,
+      selector: '#wrong',
+      suppressNotFoundErrors: true
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: false) does not suppress NoSuchElementError', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '#wrong'}),
+      response: JSON.stringify({
+        value: []
+      })
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'NoSuchElementError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '#wrong',
+      suppressNotFoundErrors: false
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: true) does not suppress other errors', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '@wrong'}),
+      response: {
+        value: {
+          error: 'invalid selector',
+          message: 'invalid selector',
+          stacktrace: ''
+        }
+      },
+      statusCode: 400
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'InvalidSelectorError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '@wrong',
+      suppressNotFoundErrors: true
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+
+  it('test .element.find(suppressNotFoundErrors: false) does not suppress other errors', async function() {
+    Mocks.createNewW3CSession();
+    MockServer.addMock({
+      url: '/session/13521-10219-202/elements',
+      method: 'POST',
+      postdata: JSON.stringify({using: 'css selector', value: '@wrong'}),
+      response: {
+        value: {
+          error: 'invalid selector',
+          message: 'invalid selector',
+          stacktrace: ''
+        }
+      },
+      statusCode: 400
+    });
+
+    let globalReporterCalled = false;
+
+    const globals = {
+      reporter(results) {
+        globalReporterCalled = true;
+        assert.strictEqual(results.lastError.name, 'InvalidSelectorError');
+      },
+      waitForConditionTimeout: 100,
+      selector: '@wrong',
+      suppressNotFoundErrors: false
+    };
+    const testsPath = [
+      path.join(__dirname, '../../../../sampletests/webelement/findWithSuppressNotFoundErrors.js')
+    ];
+
+    await NightwatchClient.runTests(testsPath, settings({
+      globals,
+      output_folder: 'output',
+      selenium_host: null
+    }));
+
+    assert.strictEqual(globalReporterCalled, true);
+  });
+});
diff --git a/test/src/api/commands/web-element/testGetAccessibleName.js b/test/src/api/commands/web-element/testGetAccessibleName.js
index 5b3bca5a..cd1c820b 100644
--- a/test/src/api/commands/web-element/testGetAccessibleName.js
+++ b/test/src/api/commands/web-element/testGetAccessibleName.js
@@ -65,6 +65,31 @@ describe('element().getAccessibleName() command', function () {
     assert.strictEqual(resultValue, 'Signup');
   });
 
+  it('test .element().getComputedLabel() alias', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/0/computedlabel',
+      method: 'GET',
+      response: JSON.stringify({
+        value: 'Signup'
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').getComputedLabel();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+    assert.strictEqual(typeof resultPromise.value, 'object');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, 'Signup');
+
+    const resultValue = await resultPromise.value;
+    assert.strictEqual(resultValue, 'Signup');
+  });
+
   it('test .element().find().getAccessibleName()', async function() {
     MockServer.addMock({
       url: '/session/13521-10219-202/element/1/computedlabel',
diff --git a/test/src/api/commands/web-element/testGetAriaRole.js b/test/src/api/commands/web-element/testGetAriaRole.js
index 7cddaad3..005803b6 100644
--- a/test/src/api/commands/web-element/testGetAriaRole.js
+++ b/test/src/api/commands/web-element/testGetAriaRole.js
@@ -38,7 +38,7 @@ describe('element().getAriaRole() command', function () {
     assert.strictEqual(resultValue, 'signupSection');
   });
 
-  it('test .element().ariaRole()', async function() {
+  it('test .element().ariaRole() alias', async function() {
     MockServer.addMock({
       url: '/session/13521-10219-202/element/0/computedrole',
       method: 'GET',
@@ -62,6 +62,30 @@ describe('element().getAriaRole() command', function () {
     assert.strictEqual(resultValue, 'signupSection');
   });
 
+  it('test .element().getComputedRole() alias', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/0/computedrole',
+      method: 'GET',
+      response: JSON.stringify({
+        value: 'signupSection'
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').getComputedRole();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, 'signupSection');
+
+    const resultValue = await resultPromise.value;
+    assert.strictEqual(resultValue, 'signupSection');
+  });
+
   it('test .element().find().getAriaRole()', async function() {
     MockServer.addMock({
       url: '/session/13521-10219-202/element/1/computedrole',
diff --git a/test/src/api/commands/web-element/testIsActive.js b/test/src/api/commands/web-element/testIsActive.js
new file mode 100644
index 00000000..b0ba235a
--- /dev/null
+++ b/test/src/api/commands/web-element/testIsActive.js
@@ -0,0 +1,130 @@
+const assert = require('assert');
+const {WebElement} = require('selenium-webdriver');
+const MockServer  = require('../../../../lib/mockserver.js');
+const CommandGlobals = require('../../../../lib/globals/commands-w3c.js');
+const common = require('../../../../common.js');
+const Element = common.require('element/index.js');
+
+describe('element().isActive() command', function() {
+  before(function (done) {
+    CommandGlobals.beforeEach.call(this, done);
+  });
+
+  after(function (done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('test .element().isActive() active', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/active',
+      method: 'GET',
+      response: JSON.stringify({
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': '0'
+        }
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').isActive();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, true);
+  });
+
+  it('test .element().isActive() not active', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/active',
+      method: 'GET',
+      response: JSON.stringify({
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': 'random-elem'
+        }
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').isActive();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, false);
+  });
+
+  it('test .element().find().isActive()', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/active',
+      method: 'GET',
+      response: JSON.stringify({
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': '1'
+        }
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').find('#helpBtn').isActive();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, true);
+  });
+
+  it('test .element.find().isActive() not active', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/active',
+      method: 'GET',
+      response: JSON.stringify({
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': 'random-elem'
+        }
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element.find('#signupSection').isActive();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    const result = await resultPromise;
+    assert.strictEqual(result instanceof WebElement, false);
+    assert.strictEqual(result, false);
+  });
+
+  it('test .element().isActive() assert', async function() {
+    MockServer.addMock({
+      url: '/session/13521-10219-202/element/active',
+      method: 'GET',
+      response: JSON.stringify({
+        value: {
+          'element-6066-11e4-a52e-4f735466cecf': '0'
+        }
+      })
+    }, true);
+
+    const resultPromise = this.client.api.element('#signupSection').isActive();
+    assert.strictEqual(resultPromise instanceof Element, false);
+    assert.strictEqual(typeof resultPromise.find, 'undefined');
+
+    assert.strictEqual(resultPromise instanceof Promise, false);
+    assert.strictEqual(typeof resultPromise.then, 'function');
+
+    assert.strictEqual(await resultPromise.assert.equals(true), true);
+    assert.strictEqual(await resultPromise.assert.not.equals(false), true);
+  });
+});
diff --git a/test/src/api/commands/web-element/testIsPresent.js b/test/src/api/commands/web-element/testIsPresent.js
index b9a18ae0..3a355783 100644
--- a/test/src/api/commands/web-element/testIsPresent.js
+++ b/test/src/api/commands/web-element/testIsPresent.js
@@ -5,7 +5,6 @@ const MockServer  = require('../../../../lib/mockserver.js');
 const CommandGlobals = require('../../../../lib/globals/commands-w3c.js');
 const common = require('../../../../common.js');
 const Element = common.require('element/index.js');
-const Utils = common.require('./utils');
 const NightwatchClient = common.require('index.js');
 const {settings} = common;
 
diff --git a/test/src/index/testRequest.js b/test/src/index/testRequest.js
index a0b72a8a..a9b2ce02 100644
--- a/test/src/index/testRequest.js
+++ b/test/src/index/testRequest.js
@@ -320,7 +320,7 @@ describe('test HttpRequest', function() {
     assert.strictEqual(secondRequest.reqOptions.agent, opts.agent);
   });
 
-  it.only('keep alive user agent changes when request protocol changes',  function() {
+  it('keep alive user agent changes when request protocol changes',  function() {
     const optionsHttp = {
       path: '/session',
       method: 'POST',
diff --git a/types/tests/customCommands.test-d.ts b/types/tests/customCommands.test-d.ts
new file mode 100644
index 00000000..67093847
--- /dev/null
+++ b/types/tests/customCommands.test-d.ts
@@ -0,0 +1,58 @@
+import {expectType} from 'tsd';
+import {CustomCommandInstance, NightwatchCallbackResult, NightwatchCustomCommandsModel} from '..';
+
+export class AngularCommand implements NightwatchCustomCommandsModel {
+  async command(this: CustomCommandInstance, listName: string, cb = function<T>(r: T) {return r}) {
+    // Script to be executed in the browser
+    const script = function(listName: string) {
+      // executed in the browser context
+      // eslint-disable-next-line
+      var elements = document.querySelectorAll('*[ng-repeat$="'+listName+'"]');
+
+      if (elements) {return elements}
+
+      return null;
+    };
+
+    // Arguments to be passed to the script function above
+    const args: [string] = [listName];
+
+    // Callback to be called when the script finishes its execution,
+    // with the result returned by the script passed as argument.
+    const callback = async function(result: any) {
+      const cbResult = await cb(result);
+
+      if (cbResult.value) {
+        return cbResult.value;
+      }
+
+      return cbResult;
+    };
+
+    // Execute the script defined above, along with arguments and
+    // the callback function.
+    await this.api.executeScript(script, args, callback);
+
+
+    // CUSTOM COMMAND WITH `this.httpRequest` EXAMPLE
+    const response = await this.httpRequest({
+      path: '/session/:sessionId/url',
+      sessionId: this.api.sessionId,
+      method: 'POST',
+      data: {
+        url: 'https://nightwatchjs.org'
+      }
+    });
+    expectType<unknown>(response);
+
+    // CUSTOM COMMAND WITH `this.transportActions` EXAMPLE (still incomplete)
+    const currentUrl = await (this.transportActions as TransportActions).getCurrentUrl();
+    expectType<NightwatchCallbackResult<string>>(currentUrl);
+
+    this.complete('hello', 1, true, {value: null});
+  }
+}
+
+interface TransportActions {
+  getCurrentUrl(): Promise<NightwatchCallbackResult<string>>;
+}
diff --git a/types/tests/page-object.test-d.ts b/types/tests/page-object.test-d.ts
new file mode 100644
index 00000000..17cfbffa
--- /dev/null
+++ b/types/tests/page-object.test-d.ts
@@ -0,0 +1,79 @@
+import {expectError, expectType} from 'tsd';
+import {EnhancedPageObject, PageObjectModel, Awaitable, NightwatchAPI} from '..';
+
+// Page object file
+const fileUploadPageElements = {
+  fileUploadInput: 'input#file-upload',
+  submitButton: 'input#file-submit',
+  uploadFiles: '#uploaded-files'
+};
+
+const menuSection = {
+  selector: 'nav',
+  elements: {
+    home: 'a[href="/"]',
+    about: 'a[href="/about"]'
+  }
+};
+
+const fileUploadPage = {
+  url(this: EnhancedPageObject) {
+    return `${this.api.launch_url}/upload`;
+  },
+  elements: fileUploadPageElements,
+  sections: {
+    menu: menuSection
+  }
+} satisfies PageObjectModel;
+
+export interface FileUploadPage extends
+  EnhancedPageObject<{}, typeof fileUploadPageElements, typeof fileUploadPage.sections, {}, () => string> {} // eslint-disable-line @typescript-eslint/ban-types
+
+export default fileUploadPage;
+
+
+// Declare the newly created page object in the NightwatchCustomPageObjects interface.
+// This will allow you to access the page object type in your test files.
+declare module '..' {
+  interface NightwatchCustomPageObjects {
+    FileUpload(): FileUploadPage;
+  }
+  interface NightwatchCustomCommands {
+    uploadFile1(selector: string, filePath: string): NightwatchAPI;
+    uploadFile2(selector: string, filePath: string): Awaitable<NightwatchAPI, null>;
+    upload: {
+      file(selector: string, filePath: string): NightwatchAPI;
+    }
+  }
+}
+
+
+// test file
+describe('File Upload', function() {
+  it('File Upload test', function() {
+    const fileUploadPage = browser.page.FileUpload();
+    const url = fileUploadPage.url();
+
+    fileUploadPage
+      .navigate(url)
+      .uploadFile('@fileUploadInput', 'test.txt')
+      // alternate way of passing an element instead of '@submitButton'
+      .click(fileUploadPage.elements.submitButton)
+      .expect.element('@uploadFiles').text.to.equal('test.txt');
+
+    // test custom commands over page object
+    expectType<NightwatchAPI>(fileUploadPage.uploadFile1('@fileUploadInput', 'test2.txt'));
+    expectType<Awaitable<NightwatchAPI, null>>(fileUploadPage.uploadFile2('@fileUploadInput', 'test2.txt'));
+    // should fail ideally but succeeding
+    expectType<NightwatchAPI>(fileUploadPage.upload.file('@fileUploadInput', 'test2.txt'));
+
+    // test custom commands over page object sections
+    const menuSection = fileUploadPage.section.menu;
+    expectType<NightwatchAPI>(menuSection.uploadFile1('@fileUploadInput', 'test2.txt'));
+    expectType<Awaitable<NightwatchAPI, null>>(menuSection.uploadFile2('@fileUploadInput', 'test2.txt'));
+    // should fail because the namespaces from custom commands are not loaded directly into the section
+    expectError(menuSection.upload.file('@fileUploadInput', 'test2.txt'));
+
+    browser.end();
+  });
+});
diff --git a/types/tests/webElement.test-d.ts b/types/tests/webElement.test-d.ts
index 1ddcc172..6bd10a5e 100644
--- a/types/tests/webElement.test-d.ts
+++ b/types/tests/webElement.test-d.ts
@@ -163,6 +163,7 @@ describe('new element() api', function () {
     expectType<ElementValue<boolean>>(elem.isSelected());
     expectType<ElementValue<boolean>>(elem.isVisible());
     expectType<ElementValue<boolean>>(elem.isDisplayed());
+    expectType<ElementValue<boolean>>(elem.isActive());
 
     expectType<ElementValue<ScopedElementRect>>(elem.getRect());
     expectType<ElementValue<ScopedElementRect>>(elem.rect());
@@ -171,8 +172,10 @@ describe('new element() api', function () {
 
     expectType<ElementValue<string>>(elem.getAccessibleName());
     expectType<ElementValue<string>>(elem.accessibleName());
+    expectType<ElementValue<string>>(elem.getComputedLabel());
     expectType<ElementValue<string>>(elem.getAriaRole());
     expectType<ElementValue<string>>(elem.ariaRole());
+    expectType<ElementValue<string>>(elem.getComputedRole());
     expectType<ElementValue<string>>(elem.getCssProperty('height'));
     expectType<ElementValue<string>>(elem.css('height'));
     expectType<ElementValue<string>>(elem.getCssValue('height'));