Skip to content

Commit d689a40

Browse files
fix(observability): lint, tighten regression tests, and broaden helper coverage
- src/utils/helper.js: satisfy padding-line-between-statements; log a debug message when args[0] is an unparseable JSON string; clarify in the doc-comment why only args[0] is inspected. - nightwatch/globals.js: clarify the eventData===null fallback behavior matches the pre-fix code (status defaults to 'passed'). - test/src/test-observability/sendTestRunEvent.js: tighten tests 3 & 4 to lock failure_reason content and backtrace shape — exactly the wire fields the original bug malformed. - test/src/utils/helper.js: add 10 unit tests for isSuppressedFailure covering null/undefined/primitive args[0], malformed JSON strings, object/string opt-out shapes, and falsy variants. Both call sites (sendTestRunEvent and setSessionStatus) are covered by reference.
1 parent f31d6f4 commit d689a40

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

nightwatch/globals.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,9 @@ module.exports = {
310310
// Skip commands the caller opted out of via suppressNotFoundErrors:true
311311
// and trust Nightwatch's envelope-level rollup over a stray command-level
312312
// fail status. Same defect shape as testObservability.js sendTestRunEvent.
313+
// When eventData itself is missing (session aborted before TestRunStarted
314+
// populated the envelope), failedCommand stays null and status defaults
315+
// to 'passed' — matches the pre-fix behaviour for that edge case.
313316
const failedCommand = eventData?.commands && Array.isArray(eventData.commands)
314317
? eventData.commands.find(cmd => cmd.status === 'fail' && !helper.isSuppressedFailure(cmd))
315318
: null;

src/utils/helper.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,24 @@ exports.isObject = value => (!this.isUndefined(value) && value.constructor === O
6565
// A Nightwatch command is recorded with status:'fail' even when the caller
6666
// explicitly opted into "not found is fine" via `suppressNotFoundErrors:true`.
6767
// Those commands carry no failure semantics for the test and must not flip
68-
// test/session status. `args[0]` is the command options object — Nightwatch
69-
// serializes it to a JSON string in some reporter paths, so accept both shapes.
68+
// test/session status. Only `args[0]` is inspected — Nightwatch's reporter
69+
// always serializes element-command options as the first positional argument,
70+
// and either as an object literal or as a JSON-encoded string depending on
71+
// the reporter path. Both shapes must be accepted.
7072
exports.isSuppressedFailure = (cmd) => {
7173
if (!cmd || cmd.status !== 'fail' || !Array.isArray(cmd.args) || cmd.args.length === 0) {return false}
7274
const first = cmd.args[0];
7375
let opts = first;
7476
if (typeof first === 'string') {
75-
try {opts = JSON.parse(first)} catch (e) {return false}
77+
try {
78+
opts = JSON.parse(first);
79+
} catch (e) {
80+
Logger.debug(`isSuppressedFailure: could not parse args[0] for cmd ${cmd.name || '<unnamed>'}: ${e.message}`);
81+
82+
return false;
83+
}
7684
}
85+
7786
return !!(opts && typeof opts === 'object' && opts.suppressNotFoundErrors === true);
7887
};
7988

test/src/test-observability/sendTestRunEvent.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ describe('TestObservability - sendTestRunEvent (suppressNotFoundErrors)', functi
118118

119119
assert.strictEqual(this.uploaded.test_run.result, 'failed');
120120
assert.strictEqual(this.uploaded.test_run.failure_type, 'AssertionError');
121+
// Lock the wire-shape that the original bug malformed (failure_reason:null,
122+
// backtrace:["",""]). The patch must propagate the real failure detail.
123+
assert.strictEqual(this.uploaded.test_run.failure_reason, 'Expected title to contain "Google"');
124+
assert.deepStrictEqual(
125+
this.uploaded.test_run.failure[0].backtrace,
126+
['Expected title to contain "Google"', 'AssertionError']
127+
);
121128
});
122129

123130
it('still marks the test failed when a non-suppressed command failed alongside a suppressed one', async () => {
@@ -145,7 +152,13 @@ describe('TestObservability - sendTestRunEvent (suppressNotFoundErrors)', functi
145152
);
146153

147154
assert.strictEqual(this.uploaded.test_run.result, 'failed');
148-
// failedCommand must skip the suppressed one and pick the real one.
155+
// failedCommand must skip the suppressed one and pick the real one — confirm
156+
// by asserting the failure_reason references the mandatory selector, not the
157+
// suppressed optional one.
149158
assert.strictEqual(this.uploaded.test_run.failure_type, 'UnhandledError');
159+
assert.ok(
160+
this.uploaded.test_run.failure_reason.includes('#mandatory'),
161+
`expected failure_reason to reference the real failure, got: ${this.uploaded.test_run.failure_reason}`
162+
);
150163
});
151164
});

test/src/utils/helper.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,69 @@ describe('isBrowserstackInfra', () => {
353353
});
354354

355355
});
356+
357+
describe('isSuppressedFailure', () => {
358+
// Shared primitive used by both src/testObservability.js (sendTestRunEvent)
359+
// and nightwatch/globals.js (setSessionStatus). Covers both call sites by
360+
// reference — any regression here breaks both surfaces equivalently.
361+
let isSuppressedFailure;
362+
before(() => {
363+
isSuppressedFailure = require('../../../src/utils/helper').isSuppressedFailure;
364+
});
365+
366+
it('returns false for null / undefined commands', () => {
367+
expect(isSuppressedFailure(null)).to.be.false;
368+
expect(isSuppressedFailure(undefined)).to.be.false;
369+
});
370+
371+
it('returns false for commands that did not fail', () => {
372+
expect(isSuppressedFailure({status: 'pass', args: [{suppressNotFoundErrors: true}]})).to.be.false;
373+
});
374+
375+
it('returns false when args is missing or empty', () => {
376+
expect(isSuppressedFailure({status: 'fail'})).to.be.false;
377+
expect(isSuppressedFailure({status: 'fail', args: []})).to.be.false;
378+
});
379+
380+
it('returns false when args is not an array', () => {
381+
expect(isSuppressedFailure({status: 'fail', args: {suppressNotFoundErrors: true}})).to.be.false;
382+
expect(isSuppressedFailure({status: 'fail', args: 'not-an-array'})).to.be.false;
383+
});
384+
385+
it('returns false when args[0] is a primitive', () => {
386+
expect(isSuppressedFailure({status: 'fail', args: [null]})).to.be.false;
387+
expect(isSuppressedFailure({status: 'fail', args: [42]})).to.be.false;
388+
expect(isSuppressedFailure({status: 'fail', args: [true]})).to.be.false;
389+
});
390+
391+
it('returns true when args[0] is an object with suppressNotFoundErrors:true', () => {
392+
expect(isSuppressedFailure({
393+
status: 'fail',
394+
args: [{selector: '#x', suppressNotFoundErrors: true, timeout: 2000}, null]
395+
})).to.be.true;
396+
});
397+
398+
it('returns true when args[0] is a JSON string carrying suppressNotFoundErrors:true', () => {
399+
expect(isSuppressedFailure({
400+
status: 'fail',
401+
args: ['{"selector":"#x","suppressNotFoundErrors":true,"timeout":2000}', null]
402+
})).to.be.true;
403+
});
404+
405+
it('returns false when args[0] is a malformed JSON string (safe default)', () => {
406+
// Unparseable strings default to "not suppressed" — i.e., the test stays
407+
// failed. That's the safer direction, matches the rest of the guard's
408+
// bias to under-suppress rather than over-suppress.
409+
expect(isSuppressedFailure({status: 'fail', args: ['{this is not json']})).to.be.false;
410+
});
411+
412+
it('returns false when args[0] is an object without suppressNotFoundErrors', () => {
413+
expect(isSuppressedFailure({status: 'fail', args: [{selector: '#x', timeout: 2000}]})).to.be.false;
414+
});
415+
416+
it('returns false when suppressNotFoundErrors is falsy', () => {
417+
expect(isSuppressedFailure({status: 'fail', args: [{suppressNotFoundErrors: false}]})).to.be.false;
418+
expect(isSuppressedFailure({status: 'fail', args: [{suppressNotFoundErrors: 'true'}]})).to.be.false;
419+
});
420+
421+
});

0 commit comments

Comments
 (0)