Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions detox/detox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ declare global {
interface DetoxSessionConfig {
autoStart?: boolean;
debugSynchronization?: number;
ignoreUnexpectedMessages?: boolean;
server?: string;
sessionId?: string;
}
Expand Down
12 changes: 9 additions & 3 deletions detox/src/client/AsyncWebSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ const DEFAULT_SEND_OPTIONS = {
};

class AsyncWebSocket {
constructor(url) {
constructor({ url, ignoreUnexpectedMessages = false }) {
this._url = url;
this._ws = null;
this._eventCallbacks = {};
this._messageIdCounter = 0;
this._opening = null;
this._closing = null;
this._abortedMessageIds = new Set();
this._ignoreUnexpectedMessages = ignoreUnexpectedMessages;

this.inFlightPromises = {};
}
Expand Down Expand Up @@ -253,8 +254,13 @@ class AsyncWebSocket {
if (this._abortedMessageIds.has(json.messageId)) {
log.debug({ messageId: json.messageId }, `late response`);
} else {
throw new DetoxRuntimeError('Unexpected message received over the web socket: ' + json.type);
}
const errorMessage = 'Unexpected message received over the web socket: ' + json.type;
if (this._ignoreUnexpectedMessages) {
log.warn({ messageId: json.messageId, type: json.type }, errorMessage + ' (ignored due to configuration)');
} else {
throw new DetoxRuntimeError(errorMessage);
}
}
}
} catch (error) {
this.rejectAll(new DetoxRuntimeError({
Expand Down
35 changes: 34 additions & 1 deletion detox/src/client/AsyncWebSocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('AsyncWebSocket', () => {
};

AsyncWebSocket = require('./AsyncWebSocket');
aws = new AsyncWebSocket(config.server);
aws = new AsyncWebSocket({ url: config.server });
log = require('../utils/logger');
});

Expand Down Expand Up @@ -385,6 +385,39 @@ describe('AsyncWebSocket', () => {
delete error.stack;
expect(error).toMatchSnapshot();
});

it('should throw on unexpected message types by default', async () => {
await connect();

const response = aws.send(generateRequest());
socket.mockMessage({ type: 'unknownMessageType', messageId: 999 });

await expect(response).rejects.toThrow('Unexpected message received over the web socket: unknownMessageType');
});

it('should log warning instead of throwing when ignoreUnexpectedMessages is enabled via options', async () => {
aws = new AsyncWebSocket({ url: config.server, ignoreUnexpectedMessages: true });
await connect();

socket.mockMessage({ type: 'unknownMessageType', messageId: 999 });

expect(log.warn).toHaveBeenCalledWith(
{ messageId: 999, type: 'unknownMessageType' },
'Unexpected message received over the web socket: unknownMessageType (ignored due to configuration)'
);
});

it('should still log debug for late responses when ignoreUnexpectedMessages is enabled', async () => {
aws = new AsyncWebSocket({ url: config.server, ignoreUnexpectedMessages: true });
await connect();
aws.send(generateRequest(1));
aws.resetInFlightPromises();

socket.mockMessage({ type: 'someReply', messageId: 1 });

expect(log.debug).toHaveBeenCalledWith({ messageId: 1 }, 'late response');
expect(log.warn).not.toHaveBeenCalled();
});
});

function connect() {
Expand Down
5 changes: 3 additions & 2 deletions detox/src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ class Client {
* @param {number} debugSynchronization
* @param {string} server
* @param {string} sessionId
* @param {boolean} [ignoreUnexpectedMessages]
*/
constructor({ debugSynchronization, server, sessionId }) {
constructor({ debugSynchronization, server, sessionId, ignoreUnexpectedMessages }) {
this._onAppConnected = this._onAppConnected.bind(this);
this._onAppReady = this._onAppReady.bind(this);
this._onAppUnresponsive = this._onAppUnresponsive.bind(this);
Expand All @@ -40,7 +41,7 @@ class Client {
this._appTerminationHandle = null;

this._successfulTestRun = true; // flag for cleanup
this._asyncWebSocket = new AsyncWebSocket(server);
this._asyncWebSocket = new AsyncWebSocket({ url: server, ignoreUnexpectedMessages });
this._serverUrl = server;

this.setEventCallback('appConnected', this._onAppConnected);
Expand Down
1 change: 1 addition & 0 deletions detox/src/configuration/collectCliConfig.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const _ = require('lodash');

const { DetoxConfigErrorComposer } = require('../errors');

Check warning on line 3 in detox/src/configuration/collectCliConfig.js

View workflow job for this annotation

GitHub Actions / Linux

'DetoxConfigErrorComposer' is assigned a value but never used

Check warning on line 3 in detox/src/configuration/collectCliConfig.js

View workflow job for this annotation

GitHub Actions / Linux

'DetoxConfigErrorComposer' is assigned a value but never used
const argparse = require('../utils/argparse');

const asBoolean = (value) => {
Expand Down Expand Up @@ -76,6 +76,7 @@
useCustomLogger: asBoolean(get('use-custom-logger')),
retries: asNumber(get('retries')),
start: get('start'),
ignoreUnexpectedWsMessages: asBoolean(get('ignore-unexpected-ws-messages')),
repl,
inspectBrk,
}, _.isUndefined);
Expand Down
1 change: 1 addition & 0 deletions detox/src/configuration/collectCliConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe('collectCliConfig', () => {
...asBoolean(['useCustomLogger', 'DETOX_USE_CUSTOM_LOGGER', 'use-custom-logger']),
...asBoolean(['inspectBrk', 'DETOX_INSPECT_BRK', 'inspect-brk']),
...asString( ['start', 'DETOX_START', 'start']),
...asBoolean(['ignoreUnexpectedWsMessages', 'DETOX_IGNORE_UNEXPECTED_WS_MESSAGES', 'ignore-unexpected-ws-messages']),
...asBooleanEnum(['repl', 'DETOX_REPL', 'repl'], ['auto']),
])('.%s property' , (key, envName, argName, input, expected) => {
beforeEach(() => {
Expand Down
11 changes: 11 additions & 0 deletions detox/src/configuration/composeSessionConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,21 @@ async function composeSessionConfig(options) {
}
}

if (session.ignoreUnexpectedMessages != null) {
const value = session.ignoreUnexpectedMessages;
if (typeof value !== 'boolean') {
throw errorComposer.invalidIgnoreUnexpectedMessagesProperty();
}
}

if (Number.parseInt(cliConfig.debugSynchronization, 10) >= 0) {
session.debugSynchronization = +cliConfig.debugSynchronization;
}

if (cliConfig.ignoreUnexpectedWsMessages != null) {
session.ignoreUnexpectedMessages = cliConfig.ignoreUnexpectedWsMessages;
}

const result = {
autoStart: !session.server,
debugSynchronization: 10000,
Expand Down
59 changes: 58 additions & 1 deletion detox/src/configuration/composeSessionConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,62 @@ describe('composeSessionConfig', () => {
});
});
});
});

describe('ignoreUnexpectedMessages', function () {
describe('by default', () => {
it('should be undefined', async () => {
const config = await compose();
expect(config.ignoreUnexpectedMessages).toBeUndefined();
});
});

it('should pass validations', async () => {
globalConfig.session = { ignoreUnexpectedMessages: 'true' };
await expect(compose()).rejects.toThrow(errorComposer.invalidIgnoreUnexpectedMessagesProperty());

globalConfig.session = { ignoreUnexpectedMessages: 1 };
await expect(compose()).rejects.toThrow(errorComposer.invalidIgnoreUnexpectedMessagesProperty());
});

describe('when defined in global config', () => {
beforeEach(() => {
globalConfig.session = { ignoreUnexpectedMessages: true };
});

it('should use that value', async () => {
expect(await compose()).toMatchObject({
ignoreUnexpectedMessages: true,
});
});

describe('and in local config', () => {
beforeEach(() => {
localConfig.session = { ignoreUnexpectedMessages: false };
});

it('should use the local config value', async () => {
expect(await compose()).toMatchObject({
ignoreUnexpectedMessages: false,
});
});

describe('and in CLI config', () => {
it('should use CLI config value when true', async () => {
cliConfig.ignoreUnexpectedWsMessages = true;
expect(await compose()).toMatchObject({
ignoreUnexpectedMessages: true,
});
});

it('should use CLI config value when false', async () => {
localConfig.session = { ignoreUnexpectedMessages: true };
cliConfig.ignoreUnexpectedWsMessages = false;
expect(await compose()).toMatchObject({
ignoreUnexpectedMessages: false,
});
});
});
});
});
});
});
12 changes: 12 additions & 0 deletions detox/src/errors/DetoxConfigErrorComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,18 @@ Examine your Detox config${this._atPath()}`,
});
}

invalidIgnoreUnexpectedMessagesProperty() {
return new DetoxConfigError({
message: `session.ignoreUnexpectedMessages should be a boolean value`,
hint: `Check that in your Detox config${this._atPath()}`,
inspectOptions: { depth: 3 },
debugInfo: _.omitBy({
session: _.get(this.contents, ['session']),
...this._focusOnConfiguration(c => _.pick(c, ['session'])),
}, _.isEmpty),
});
}

invalidTestRunnerProperty(isGlobal) {
const testRunner = _.get(
isGlobal
Expand Down
41 changes: 41 additions & 0 deletions detox/src/errors/DetoxConfigErrorComposer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,47 @@ describe('DetoxConfigErrorComposer', () => {
});
});

describe('.invalidIgnoreUnexpectedMessagesProperty', () => {
beforeEach(() => {
build = () => builder.invalidIgnoreUnexpectedMessagesProperty();
builder.setConfigurationName('android.release');
builder.setDetoxConfig({
configurations: {
'android.release': {
type: 'android.emulator',
device: {
avdName: 'Pixel_2_API_29',
},
session: {
ignoreUnexpectedMessages: 'true',
},
}
}
});
});

it('should create a generic error, if the config location is not known', () => {
expect(build()).toMatchSnapshot();
});

it('should create an error with a hint, if the config location is known', () => {
builder.setDetoxConfigPath('/home/detox/myproject/.detoxrc.json');
expect(build()).toMatchSnapshot();
});

it('should point to global session if there is one', () => {
builder.setDetoxConfig({
session: {
ignoreUnexpectedMessages: 'not a boolean',
},
configurations: {},
});

builder.setDetoxConfigPath('/home/detox/myproject/.detoxrc.json');
expect(build()).toMatchSnapshot();
});
});

describe('.invalidTestRunnerProperty', () => {
beforeEach(() => {
build = (isGlobal) => builder.invalidTestRunnerProperty(isGlobal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,53 @@ HINT: Check that in your Detox config at path:
}]
`;

exports[`DetoxConfigErrorComposer (from composeSessionConfig) .invalidIgnoreUnexpectedMessagesProperty should create a generic error, if the config location is not known 1`] = `
[DetoxConfigError: session.ignoreUnexpectedMessages should be a boolean value

HINT: Check that in your Detox config at path:
/home/detox/myproject/.detoxrc.json

{
configurations: {
'android.release': {
session: {
ignoreUnexpectedMessages: 'true'
}
}
}
}]
`;

exports[`DetoxConfigErrorComposer (from composeSessionConfig) .invalidIgnoreUnexpectedMessagesProperty should create an error with a hint, if the config location is known 1`] = `
[DetoxConfigError: session.ignoreUnexpectedMessages should be a boolean value

HINT: Check that in your Detox config at path:
/home/detox/myproject/.detoxrc.json

{
configurations: {
'android.release': {
session: {
ignoreUnexpectedMessages: 'true'
}
}
}
}]
`;

exports[`DetoxConfigErrorComposer (from composeSessionConfig) .invalidIgnoreUnexpectedMessagesProperty should point to global session if there is one 1`] = `
[DetoxConfigError: session.ignoreUnexpectedMessages should be a boolean value

HINT: Check that in your Detox config at path:
/home/detox/myproject/.detoxrc.json

{
session: {
ignoreUnexpectedMessages: 'not a boolean'
}
}]
`;

exports[`DetoxConfigErrorComposer (from composeSessionConfig) .invalidServerProperty should create a generic error, if the config location is not known 1`] = `
[DetoxConfigError: session.server property is not a valid WebSocket URL

Expand Down
22 changes: 22 additions & 0 deletions docs/config/session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,25 @@ To disable this behavior (i.e. querying the app periodically), set the value to
Seeing logs like these usually indicates certain issues in your application, as explained in the [Troubleshooting Guide](../troubleshooting/synchronization.md).

For extended, more detailed information on iOS, refer to the `DetoxSync` project's [Status Documentation](https://github.com/wix-incubator/DetoxSync/blob/master/StatusDocumentation.md).

### `session.ignoreUnexpectedMessages` \[boolean]

Default: `false`.

Controls whether Detox should throw an error or log a warning when receiving unexpected WebSocket messages that don't match any registered handler or in-flight promise.

When set to `true`, unexpected messages will be logged as warnings instead of throwing a `DetoxRuntimeError`. This is particularly useful for applications with complex view hierarchies, such as React Native apps with WebViews, where legitimate messages might be received in unexpected contexts (e.g., messages from WebViews after switching back to native context).

```json
{
"session": {
"ignoreUnexpectedMessages": true
}
}
```

:::tip

This option can also be controlled via the environment variable `DETOX_IGNORE_UNEXPECTED_WS_MESSAGES=true`.

:::
Loading