diff --git a/packages/dev-middleware/src/__tests__/InspectorProxyCustomMessageHandler-test.js b/packages/dev-middleware/src/__tests__/InspectorProxyCustomMessageHandler-test.js index 5f3f8b7798d875..92c0eab475b372 100644 --- a/packages/dev-middleware/src/__tests__/InspectorProxyCustomMessageHandler-test.js +++ b/packages/dev-middleware/src/__tests__/InspectorProxyCustomMessageHandler-test.js @@ -9,9 +9,13 @@ * @oncall react_native */ +import {fetchJson} from './FetchUtils'; +import {createDebuggerMock} from './InspectorDebuggerUtils'; +import {createDeviceMock} from './InspectorDeviceUtils'; import {createAndConnectTarget} from './InspectorProtocolUtils'; import {withAbortSignalForEachTest} from './ResourceUtils'; import {baseUrlForServer, createServer} from './ServerUtils'; +import invariant from 'invariant'; import until from 'wait-for-expect'; // WebSocket is unreliable when using fake timers. @@ -76,6 +80,86 @@ describe('inspector proxy device message middleware', () => { } }); + test('middleware is created with device, debugger, and page information for synthetic reloadable page', async () => { + const createCustomMessageHandler = jest.fn().mockImplementation(() => null); + const {server} = await createServer({ + logger: undefined, + projectRoot: '', + unstable_customInspectorMessageHandler: createCustomMessageHandler, + }); + const {serverBaseUrl, serverBaseWsUrl} = serverRefUrls(server); + + let device, debugger_; + try { + device = await createDeviceMock( + `${serverBaseWsUrl}/inspector/device?device=device1&name=foo&app=bar`, + autoCleanup.signal, + ); + // Mock the device to return a normal React (Native) page + device.getPages.mockImplementation(() => [ + { + ...page, + // NOTE: 'React' is a magic string used to detect React Native pages. + title: 'React Native (mock)', + }, + ]); + + // Retrieve the full page list from device + let pageList; + await until(async () => { + pageList = (await fetchJson( + `${serverBaseUrl}/json`, + // $FlowIgnore[unclear-type] + ): any); + expect(pageList.length).toBeGreaterThan(0); + }); + invariant(pageList != null, ''); + + // Find the synthetic page + const syntheticPage = pageList.find( + ({title}) => + // NOTE: Magic string used for the synthetic page that has a stable ID + title === 'React Native Experimental (Improved Chrome Reloads)', + ); + expect(syntheticPage).not.toBeUndefined(); + + // Connect the debugger to this synthetic page + debugger_ = await createDebuggerMock( + syntheticPage.webSocketDebuggerUrl, + autoCleanup.signal, + ); + + // Ensure the middleware was created with the device information + await until(() => + expect(createCustomMessageHandler).toBeCalledWith( + expect.objectContaining({ + page: expect.objectContaining({ + id: expect.any(String), + title: syntheticPage.title, + vm: syntheticPage.vm, + app: expect.any(String), + capabilities: expect.any(Object), + }), + device: expect.objectContaining({ + appId: expect.any(String), + id: expect.any(String), + name: expect.any(String), + sendMessage: expect.any(Function), + }), + debugger: expect.objectContaining({ + userAgent: null, + sendMessage: expect.any(Function), + }), + }), + ), + ); + } finally { + device?.close(); + debugger_?.close(); + await closeServer(server); + } + }); + test('send message functions are passing messages to sockets', async () => { const handleDebuggerMessage = jest.fn(); const handleDeviceMessage = jest.fn(); diff --git a/packages/dev-middleware/src/inspector-proxy/Device.js b/packages/dev-middleware/src/inspector-proxy/Device.js index a7c4450c593da3..b4ef0ad731cad8 100644 --- a/packages/dev-middleware/src/inspector-proxy/Device.js +++ b/packages/dev-middleware/src/inspector-proxy/Device.js @@ -176,14 +176,7 @@ export default class Device { getPagesList(): $ReadOnlyArray { if (this.#lastConnectedLegacyReactNativePage) { - const reactNativeReloadablePage = { - id: REACT_NATIVE_RELOADABLE_PAGE_ID, - title: 'React Native Experimental (Improved Chrome Reloads)', - vm: "don't use", - app: this.#app, - capabilities: {}, - }; - return [...this.#pages.values(), reactNativeReloadablePage]; + return [...this.#pages.values(), this.#createSyntheticPage()]; } else { return [...this.#pages.values()]; } @@ -224,7 +217,10 @@ export default class Device { // TODO(moti): Handle null case explicitly, e.g. refuse to connect to // unknown pages. - const page: ?Page = this.#pages.get(pageId); + const page: ?Page = + pageId === REACT_NATIVE_RELOADABLE_PAGE_ID + ? this.#createSyntheticPage() + : this.#pages.get(pageId); this.#debuggerConnection = debuggerInfo; @@ -379,6 +375,19 @@ export default class Device { return page.capabilities[flag] === true; } + /** + * Returns the synthetic "React Native Experimental (Improved Chrome Reloads)" page. + */ + #createSyntheticPage(): Page { + return { + id: REACT_NATIVE_RELOADABLE_PAGE_ID, + title: 'React Native Experimental (Improved Chrome Reloads)', + vm: "don't use", + app: this.#app, + capabilities: {}, + }; + } + // Handles messages received from device: // 1. For getPages responses updates local #pages list. // 2. All other messages are forwarded to debugger as wrappedEvent.