#4451·nightwatch

Page section custom assertions retry until timeout instead of failing immediately

Author: humphreynCreated Aug 19, 2026Updated Aug 19, 2026

Description of the bug/issue

When a custom assertion is executed from a page section and the assertion fails, Nightwatch does not fail the step immediately. Instead, the test continues and the step only fails on timeout.

The same custom assertion behaves as expected when run outside a page section.

Steps to reproduce

  1. Create a page object with a section.
  2. Define a custom assertion that returns a proper Nightwatch result object with status: 0 on success or status: -1 on error.
  3. Call that custom assertion from the section.
  4. Make the assertion evaluate to false (fail).
  5. Run the test.
  6. Observe that the assertion is reported as failed, but the step does not fail immediately and only fails on timeout.

Sample test

node
**Custom assertion (custom_assertion_example.cjs):**


exports.assertion = function(selector, expected) {
  this.options = { elementSelector: true };

  this.expected = function() {
    return expected;
  };

  this.evaluate = function(value) {
    return value === expected;
  };

  this.value = function(result) {
    return result.value;
  };

  this.failure = function(result) {
    return !result || result.status !== 0 || typeof result.value !== 'string';
  };

  this.command = async function(callback) {
    // Simulates finding an element and returning 'actual-value'
    callback({ value: 'actual-value', status: 0 });
  };
};


Page object (example_page.js):
module.exports = {
  sections: {
    content: {
      selector: '.content',
      elements: {
        title: '.title'
      }
    }
  }
};


Test (example_test.js):


module.exports = {
  'custom assertion from section should fail immediately'(browser) {
    const page = browser.page.example_page();

    // This assertion fails (expected 'expected-value', got 'actual-value')
    page.section.content.assert.equalText('@title', 'expected-value');

    // Expected: test fails here immediately
    // Actual: test continues until timeout
  }
};

Command to run

bash
nightwatch test/example_test.js --verbose

Verbose Output

fundamental
──────────────────────────────────────────────────────── 
⠋ Starting ChromeDriver on port 9515...
Request POST /session Response 200 /session { sessionId: '12345abc' }
Request POST /session/12345abc/url Response 200 /session/12345abc/url
Request POST /session/12345abc/execute Response 200 { value: 'actual-value', status: 0 }
✖ FAILED [equalText] Expected element .content .title text to equal 'expected-value' - expected ' expected-value' but got 'actual-value'
[00:02.457] Retrying assertion (attempt 2/5) ... Request POST /session/12345abc/execute Response 200 { value: 'actual-value', status: 0 }
✖ FAILED [equalText] Expected element .content .title text to equal 'expected-value' - expected ' expected-value' but got 'actual-value'
[00:03.457] Retrying assertion (attempt 3/5) ... Request POST /session/12345abc/execute Response 200 { value: 'actual-value', status: 0 }
✖ FAILED [equalText] Expected element .content .title text to equal 'expected-value' - expected ' expected-value' but got 'actual-value'
[00:04.457] Retrying assertion (attempt 4/5) ... Request POST /session/12345abc/execute Response 200 { value: 'actual-value', status: 0 }
✖ FAILED [equalText] Expected element .content .title text to equal 'expected-value' - expected ' expected-value' but got 'actual-value'
[00:05.457] Retrying assertion (attempt 5/5) ... Request POST /session/12345abc/execute Response 200 { value: 'actual-value', status: 0 }
✖ FAILED [equalText] Expected element .content .title text to equal 'expected-value' - expected ' expected-value' but got 'actual-value'
[00:05.500] FAILED: custom assertion from section should fail immediately - retryAssertionTimeout (5000ms) exceeded
Request DELETE /session/12345abc Response 200 /session/12345abc
FAILED  1 assertions failed, 0 passed

Nightwatch Configuration

node
module.exports = {
  src_folders: ['test'],
  page_objects_path: ['test/pages'],
  custom_assertions_path: ['test/custom_assertions'],
  test_settings: {
    default: {
      desiredCapabilities: {
        browserName: 'chrome'
      }
    }
  }
};

Nightwatch.js Version

3.16.0

Node Version

24.16.0

Browser

Chrome Version 151.0.7922.138

Operating System

MacOS 26.5.2 (25F84)

Additional Information

We traced the likely cause to lib/page-object/command-wrapper.js in the createWrapper() method. File: lib/page-object/command-wrapper.js Lines: 88-115 (Nightwatch 3.16.0) Relevant code block:

javascript
if ((result instanceof Promise) && (self.parent.constructor.name === 'Page' || isES6AsyncTestcase)) {
  // lines 94-112
  Object.assign(result, self.parent);
  const parentPrototype = Object.getPrototypeOf(self.parent);
  Object.getOwnPropertyNames(parentPrototype).forEach((propertyName) => {
    if (propertyName === 'constructor') {
      return;
    }
    const propertyDescriptor = Object.getOwnPropertyDescriptor(parentPrototype, propertyName);
    Object.defineProperty(result, propertyName, propertyDescriptor);
  });

  return result; // line 112
}

return self.parent; // line 115

When a section-scoped custom assertion returns a rejected Promise, the condition on line 94 evaluates to false because the parent is a Section, not a Page, and typically isES6AsyncTestcase is false. The rejected Promise is therefore not returned. Instead, self.parent is returned on line 115, allowing the test to continue until timeout.

Page-scoped assertions work correctly because self.parent.constructor.name === 'Page' evaluates to true, the Promise is returned, and rejection is properly propagated.

This affects all custom assertions run from page sections when they fail in non-async test execution.