UNPKG

expo

Version:
286 lines (247 loc) • 8.92 kB
/// <reference types="node" /> import { TextDecoder, TextEncoder } from 'node:util'; import MockWebSocket from './MockWebSocket'; import { DevToolsPluginClient } from '../DevToolsPluginClient'; import { createDevToolsPluginClient } from '../DevToolsPluginClientFactory'; import { WebSocketBackingStore } from '../WebSocketBackingStore'; // @ts-expect-error - We don't mock all properties from WebSocket globalThis.WebSocket = MockWebSocket; // @ts-ignore - TextDecoder and TextEncoder are not defined in native jest environments. globalThis.TextDecoder ??= TextDecoder; globalThis.TextEncoder ??= TextEncoder; const TEST_PROTOCOL_VERSION = 1; describe(`DevToolsPluginClient`, () => { let appClient: DevToolsPluginClient; let testCaseCounter = 0; let devServer: string; const pluginName = 'testPlugin'; beforeEach(async () => { // Connect to different devServer for each test case to avoid jest parallel test issues. testCaseCounter += 1; devServer = `localhost:${8000 + testCaseCounter}`; appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); }); afterEach(async () => { await appClient.closeAsync(); }); it('should connect to the WebSocket server', async () => { expect(appClient.isConnected()).toBe(true); }); }); describe(`DevToolsPluginClient (browser <> app)`, () => { let testCaseCounter = 0; let devServer: string; const pluginName = 'testPlugin'; let appClient: DevToolsPluginClient; let browserClient: DevToolsPluginClient; beforeEach(() => { // Connect to different devServer for each test case to avoid jest parallel test issues. testCaseCounter += 1; devServer = `localhost:${8000 + testCaseCounter}`; }); afterEach(async () => { await appClient?.closeAsync(); await browserClient?.closeAsync(); }); it('should send and receive messages', async () => { const method = 'testMethod'; const message = { foo: 'bar' }; appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); browserClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'browser', pluginName, wsStore: new WebSocketBackingStore(), }); const receivedPromise = new Promise((resolve) => { appClient.addMessageListener(method, (params) => { resolve(params); }); }); browserClient.sendMessage(method, message); const received = await receivedPromise; expect(received).toEqual(message); }); it('should support ping-pong messages', async () => { appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); browserClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'browser', pluginName, wsStore: new WebSocketBackingStore(), }); const appPromise = new Promise((resolve) => { appClient.addMessageListener('ping', (params) => { appClient.sendMessage('pong', { from: 'app' }); resolve(params); }); }); const browserPromise = new Promise((resolve) => { browserClient.addMessageListener('pong', (params) => { resolve(params); }); }); browserClient.sendMessage('ping', { from: 'browser' }); const receivedPing = await appPromise; expect(receivedPing).toEqual({ from: 'browser' }); const receivedPong = await browserPromise; expect(receivedPong).toEqual({ from: 'app' }); }); it('should not receive messages from differnet plugin', async () => { const method = 'testMethod'; const message = { foo: 'bar' }; appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); browserClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'browser', pluginName: 'pluginB', wsStore: new WebSocketBackingStore(), }); const receivedPromise = new Promise((resolve) => { appClient.addMessageListener(method, (params) => { resolve(params); }); }); browserClient.sendMessage(method, message); expect(receivedPromise).rejects.toThrow(); }); it('should only allow the latest connected client with the same plugin name to receive messages', async () => { const method = 'testMethod'; appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); const receivedMessages: any[] = []; appClient.addMessageListener(method, (params) => { receivedMessages.push(params); }); browserClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'browser', pluginName, wsStore: new WebSocketBackingStore(), }); await delayAsync(100); const browserClient2 = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'browser', pluginName, wsStore: new WebSocketBackingStore(), }); await delayAsync(100); expect(browserClient.isConnected()).toBe(false); expect(browserClient2.isConnected()).toBe(true); browserClient.sendMessage(method, { from: 'browserClient' }); browserClient2.sendMessage(method, { from: 'browserClient2' }); await delayAsync(100); expect(receivedMessages.length).toBe(1); expect(receivedMessages[0]).toEqual({ from: 'browserClient2' }); await browserClient2.closeAsync(); }); it('should terminate client from incompatible protocol version', async () => { const spyConsoleInfo = jest.spyOn(console, 'info').mockImplementation(() => {}); const spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); appClient = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName, wsStore: new WebSocketBackingStore(), }); browserClient = await createDevToolsPluginClient({ protocolVersion: -1, devServer, sender: 'browser', pluginName, wsStore: new WebSocketBackingStore(), }); await delayAsync(100); expect(spyConsoleWarn).toHaveBeenCalledWith( `Received an incompatible devtools plugin handshake message - pluginName[${pluginName}]` ); expect(browserClient.isConnected()).toBe(false); spyConsoleInfo.mockRestore(); spyConsoleWarn.mockRestore(); }); }); describe(`DevToolsPluginClient - multiplexing`, () => { let testCaseCounter = 0; let devServer: string; beforeEach(() => { // Connect to different devServer for each test case to avoid jest parallel test issues. testCaseCounter += 1; devServer = `localhost:${8000 + testCaseCounter}`; }); it('should not close the websocket connection while there are alive clients', async () => { const wsStore = new WebSocketBackingStore(); const appClient1 = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName: 'plugin1', wsStore, }); const appClient2 = await createDevToolsPluginClient({ protocolVersion: TEST_PROTOCOL_VERSION, devServer, sender: 'app', pluginName: 'plugin2', wsStore, }); expect(appClient1.isConnected()).toBe(true); expect(appClient2.isConnected()).toBe(true); expect(appClient1.getWebSocketBackingStore()).toBe(wsStore); expect(appClient2.getWebSocketBackingStore()).toBe(wsStore); expect(wsStore.refCount).toBe(2); const ws = wsStore.ws; if (!ws) { throw new Error('Null WebSocket'); } const mockClose = jest.spyOn(ws, 'close'); expect(mockClose).toHaveBeenCalledTimes(0); expect(ws.readyState).toBe(WebSocket.OPEN); await appClient1.closeAsync(); expect(mockClose).toHaveBeenCalledTimes(0); expect(wsStore.refCount).toBe(1); expect(ws.readyState).toBe(WebSocket.OPEN); await appClient2.closeAsync(); expect(mockClose).toHaveBeenCalledTimes(1); expect(wsStore.refCount).toBe(0); expect(ws.readyState).toBe(WebSocket.CLOSED); }); }); async function delayAsync(timeMs: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, timeMs)); }