Page section custom assertions retry until timeout instead of failing immediately
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
- Create a page object with a section.
- Define a custom assertion that returns a proper Nightwatch result object with
status: 0on success orstatus: -1on error. - Call that custom assertion from the section.
- Make the assertion evaluate to false (fail).
- Run the test.
- Observe that the assertion is reported as failed, but the step does not fail immediately and only fails on timeout.
Sample test
**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
nightwatch test/example_test.js --verboseVerbose Output
────────────────────────────────────────────────────────
⠋ 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 passedNightwatch Configuration
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:
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 115When 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.
Source: nightwatchjs/nightwatch