/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ describe('Bridge', () => { let Bridge; beforeEach(() => { Bridge = require('react-devtools-shared/src/bridge').default; }); // @reactVersion >=16.0 it('should shutdown properly', () => { const wall = { listen: jest.fn(() => () => {}), send: jest.fn(), }; const bridge = new Bridge(wall); const shutdownCallback = jest.fn(); bridge.addListener('shutdown', shutdownCallback); // Check that we're wired up correctly. bridge.send('reloadAppForProfiling'); jest.runAllTimers(); expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling', undefined); // Should flush pending messages and then shut down. wall.send.mockClear(); bridge.send('update', '1'); bridge.send('update', '2'); bridge.shutdown(); jest.runAllTimers(); expect(wall.send).toHaveBeenCalledWith('update', '1'); expect(wall.send).toHaveBeenCalledWith('update', '2'); expect(wall.send).toHaveBeenCalledWith('shutdown', undefined); expect(shutdownCallback).toHaveBeenCalledTimes(1); // Using a Bridge after shutdown is a lifecycle error. wall.send.mockClear(); expect(() => bridge.send('should not send')).toThrow( 'Cannot send a message through a Bridge that has been shut down.', ); expect(() => bridge.addListener('event', () => {})).toThrow( 'Cannot add a listener through a Bridge that has been shut down.', ); expect(() => bridge.emit('event')).toThrow( 'Cannot emit an event through a Bridge that has been shut down.', ); expect(() => bridge.shutdown()).toThrow( 'Cannot shut down through a Bridge that has been shut down.', ); jest.runAllTimers(); expect(wall.send).not.toHaveBeenCalled(); }); // @reactVersion >=16.0 it('validates messages received from the wall', () => { let wallListener: ((message: mixed) => void) | null = null; const wall = { listen: jest.fn(listener => { wallListener = listener; return () => {}; }), send: jest.fn(), }; const bridge = new Bridge(wall); const listener = jest.fn(); bridge.addListener('event', listener); const dispatch = (message: mixed) => { if (wallListener === null) { throw new Error('Expected the Bridge to subscribe to the wall.'); } wallListener(message); }; // Walls may share their transport with unrelated or legacy messages. dispatch(null); dispatch({type: 'event'}); expect(listener).not.toHaveBeenCalled(); expect(() => dispatch({event: 123})).toThrow( 'Bridge event names must be non-empty strings.', ); expect(() => dispatch({event: ''})).toThrow( 'Bridge event names must be non-empty strings.', ); dispatch({event: 'event', payload: 123}); expect(listener).toHaveBeenCalledWith(123); }); // @reactVersion >=16.0 it('requires Wall.listen to return a cleanup function', () => { expect( () => new Bridge({ listen: () => undefined, send: jest.fn(), }), ).toThrow('Wall.listen() must return an unlisten function.'); }); // @reactVersion >=16.0 it('flushes pending messages when wall cleanup throws', () => { const expectedError = new Error('Failed to unsubscribe'); const wall = { listen: jest.fn(() => () => { throw expectedError; }), send: jest.fn(), }; const bridge = new Bridge(wall); bridge.send('update', 'value'); expect(() => bridge.shutdown()).toThrow(expectedError); expect(wall.send).toHaveBeenCalledWith('update', 'value'); expect(wall.send).toHaveBeenCalledWith('shutdown', undefined); }); });