/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @emails oncall+react_native */ 'use strict'; jest.unmock('../NativeModules'); let BatchedBridge; let NativeModules; let fs; let parseErrorStack; const MODULE_IDS = 0; const METHOD_IDS = 1; const PARAMS = 2; const CALL_ID = 3; const assertQueue = (flushedQueue, index, moduleID, methodID, params) => { expect(flushedQueue[MODULE_IDS][index]).toEqual(moduleID); expect(flushedQueue[METHOD_IDS][index]).toEqual(methodID); expect(flushedQueue[PARAMS][index]).toEqual(params); }; // Important things to test: // // [x] Calling remote method actually queues it up on the BatchedBridge // // [x] Both error and success callbacks are invoked. // // [x] When simulating an error callback from remote method, both error and // success callbacks are cleaned up. // // [ ] Remote invocation throws if not supplying an error callback. describe('MessageQueue', function() { beforeEach(function() { jest.resetModules(); global.__fbBatchedBridgeConfig = require('../__mocks__/MessageQueueTestConfig'); BatchedBridge = require('../BatchedBridge'); NativeModules = require('../NativeModules'); fs = require('fs'); parseErrorStack = require('../../Core/Devtools/parseErrorStack'); }); it('should generate native modules', () => { NativeModules.RemoteModule1.remoteMethod('foo'); const flushedQueue = BatchedBridge.flushedQueue(); assertQueue(flushedQueue, 0, 0, 0, ['foo']); }); it('should make round trip and clear memory', function() { const onFail = jest.fn(); const onSucc = jest.fn(); // Perform communication NativeModules.RemoteModule1.promiseMethod( 'paloAlto', 'menloPark', onFail, onSucc, ); NativeModules.RemoteModule2.promiseMethod('mac', 'windows', onFail, onSucc); const resultingRemoteInvocations = BatchedBridge.flushedQueue(); // As always, the message queue has four fields expect(resultingRemoteInvocations.length).toBe(4); expect(resultingRemoteInvocations[MODULE_IDS].length).toBe(2); expect(resultingRemoteInvocations[METHOD_IDS].length).toBe(2); expect(resultingRemoteInvocations[PARAMS].length).toBe(2); expect(typeof resultingRemoteInvocations[CALL_ID]).toEqual('number'); expect(resultingRemoteInvocations[0][0]).toBe(0); // `RemoteModule1` expect(resultingRemoteInvocations[1][0]).toBe(1); // `promiseMethod` expect([ // the arguments resultingRemoteInvocations[2][0][0], resultingRemoteInvocations[2][0][1], ]).toEqual(['paloAlto', 'menloPark']); // Callbacks ids are tacked onto the end of the remote arguments. const firstFailCBID = resultingRemoteInvocations[2][0][2]; const firstSuccCBID = resultingRemoteInvocations[2][0][3]; expect(resultingRemoteInvocations[0][1]).toBe(1); // `RemoteModule2` expect(resultingRemoteInvocations[1][1]).toBe(1); // `promiseMethod` expect([ // the arguments resultingRemoteInvocations[2][1][0], resultingRemoteInvocations[2][1][1], ]).toEqual(['mac', 'windows']); const secondFailCBID = resultingRemoteInvocations[2][1][2]; const secondSuccCBID = resultingRemoteInvocations[2][1][3]; // Handle the first remote invocation by signaling failure. BatchedBridge.__invokeCallback(firstFailCBID, ['firstFailure']); // The failure callback was already invoked, the success is no longer valid expect(function() { BatchedBridge.__invokeCallback(firstSuccCBID, ['firstSucc']); }).toThrow(); expect(onFail.mock.calls.length).toBe(1); expect(onSucc.mock.calls.length).toBe(0); // Handle the second remote invocation by signaling success. BatchedBridge.__invokeCallback(secondSuccCBID, ['secondSucc']); // The success callback was already invoked, the fail cb is no longer valid expect(function() { BatchedBridge.__invokeCallback(secondFailCBID, ['secondFail']); }).toThrow(); expect(onFail.mock.calls.length).toBe(1); expect(onSucc.mock.calls.length).toBe(1); }); it('promise-returning methods (type=promise)', async function() { // Perform communication const promise1 = NativeModules.RemoteModule1.promiseReturningMethod( 'paloAlto', 'menloPark', ); const promise2 = NativeModules.RemoteModule2.promiseReturningMethod( 'mac', 'windows', ); const resultingRemoteInvocations = BatchedBridge.flushedQueue(); // As always, the message queue has four fields expect(resultingRemoteInvocations.length).toBe(4); expect(resultingRemoteInvocations[MODULE_IDS].length).toBe(2); expect(resultingRemoteInvocations[METHOD_IDS].length).toBe(2); expect(resultingRemoteInvocations[PARAMS].length).toBe(2); expect(typeof resultingRemoteInvocations[CALL_ID]).toEqual('number'); expect(resultingRemoteInvocations[0][0]).toBe(0); // `RemoteModule1` expect(resultingRemoteInvocations[1][0]).toBe(2); // `promiseReturningMethod` expect([ // the arguments resultingRemoteInvocations[2][0][0], resultingRemoteInvocations[2][0][1], ]).toEqual(['paloAlto', 'menloPark']); // For promise-returning methods, the order of callbacks is flipped from // regular async methods. const firstSuccCBID = resultingRemoteInvocations[2][0][2]; const firstFailCBID = resultingRemoteInvocations[2][0][3]; expect(resultingRemoteInvocations[0][1]).toBe(1); // `RemoteModule2` expect(resultingRemoteInvocations[1][1]).toBe(2); // `promiseReturningMethod` expect([ // the arguments resultingRemoteInvocations[2][1][0], resultingRemoteInvocations[2][1][1], ]).toEqual(['mac', 'windows']); const secondSuccCBID = resultingRemoteInvocations[2][1][2]; const secondFailCBID = resultingRemoteInvocations[2][1][3]; // Handle the first remote invocation by signaling failure. BatchedBridge.__invokeCallback(firstFailCBID, [{message: 'firstFailure'}]); // The failure callback was already invoked, the success is no longer valid expect(function() { BatchedBridge.__invokeCallback(firstSuccCBID, ['firstSucc']); }).toThrow(); await expect(promise1).rejects.toBeInstanceOf(Error); await expect(promise1).rejects.toMatchObject({message: 'firstFailure'}); // Handle the second remote invocation by signaling success. BatchedBridge.__invokeCallback(secondSuccCBID, ['secondSucc']); // The success callback was already invoked, the fail cb is no longer valid expect(function() { BatchedBridge.__invokeCallback(secondFailCBID, ['secondFail']); }).toThrow(); await promise2; }); describe('sync methods', () => { afterEach(function() { delete global.nativeCallSyncHook; }); it('throwing an exception', function() { global.nativeCallSyncHook = jest.fn(() => { throw new Error('firstFailure'); }); let error; try { NativeModules.RemoteModule1.syncMethod('paloAlto', 'menloPark'); } catch (e) { error = e; } expect(global.nativeCallSyncHook).toBeCalledTimes(1); expect(global.nativeCallSyncHook).toBeCalledWith( 0, // `RemoteModule1` 3, // `syncMethod` ['paloAlto', 'menloPark'], ); expect(error).toBeInstanceOf(Error); expect(error).toMatchObject({ message: 'firstFailure', }); }); it('returning a value', function() { global.nativeCallSyncHook = jest.fn(() => { return 'secondSucc'; }); const result = NativeModules.RemoteModule2.syncMethod('mac', 'windows'); expect(global.nativeCallSyncHook).toBeCalledTimes(1); expect(global.nativeCallSyncHook).toBeCalledWith( 1, // `RemoteModule2` 3, // `syncMethod` ['mac', 'windows'], ); expect(result).toBe('secondSucc'); }); }); });