Skip to content
Open
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
13 changes: 10 additions & 3 deletions detox/src/client/AsyncWebSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const DEFAULT_SEND_OPTIONS = {
};

class AsyncWebSocket {
constructor(url) {
constructor(url, options = {}) {
this._url = url;
this._ws = null;
this._eventCallbacks = {};
this._messageIdCounter = 0;
this._opening = null;
this._closing = null;
this._abortedMessageIds = new Set();
this._ignoreUnexpectedMessages = options.ignoreUnexpectedMessages ??
(process.env.DETOX_IGNORE_UNEXPECTED_WS_MESSAGES === 'true');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have collectCliConfig for this, we don't use process.env directly.


this.inFlightPromises = {};
}
Expand Down Expand Up @@ -253,8 +255,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
48 changes: 48 additions & 0 deletions detox/src/client/AsyncWebSocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,54 @@ 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(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 log warning instead of throwing when DETOX_IGNORE_UNEXPECTED_WS_MESSAGES env var is set', async () => {
process.env.DETOX_IGNORE_UNEXPECTED_WS_MESSAGES = 'true';
aws = new AsyncWebSocket(config.server);
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)'
);

delete process.env.DETOX_IGNORE_UNEXPECTED_WS_MESSAGES;
});

it('should still log debug for late responses when ignoreUnexpectedMessages is enabled', async () => {
aws = new AsyncWebSocket(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(server, { ignoreUnexpectedMessages });
this._serverUrl = server;

this.setEventCallback('appConnected', this._onAppConnected);
Expand Down
7 changes: 7 additions & 0 deletions detox/src/configuration/composeSessionConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ 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;
}
Expand Down
42 changes: 41 additions & 1 deletion detox/src/configuration/composeSessionConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,45 @@ 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,
});
});
});
});
});
});
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
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`.

:::