fix(dev-middleware): create custom message handler for synthetic page (#43559)

Summary:
This is a follow-up bugfix for expo/expo#27425, related to:
 - https://github.com/facebook/react-native/issues/43291
 - https://github.com/facebook/react-native/issues/43307
 - https://github.com/facebook/react-native/issues/43310
 - https://github.com/facebook/react-native/issues/43364

The middleware API works as intended and can run our extended CDP events. Unfortunately, this only applies to an actual `Page` from the device, not for the `React Native Experimental (Improved Chrome Reloads)` synthetic / virtual page.

That's because the middleware instantiation gets aborted when the page can't be found in `this.#pages.get(pageId)`, which always returns `null` for this synthetic page.

## Changelog:

[GENERAL] [FIXED] Create custom message handler for synthetic page

Pull Request resolved: https://github.com/facebook/react-native/pull/43559

Test Plan: See added test case.

Reviewed By: motiz88

Differential Revision: D55129412

Pulled By: huntie

fbshipit-source-id: 9679d8fe68f3cb4104f4a042f93612b995baddc9
This commit is contained in:
Cedric van Putten
2024-03-20 08:59:46 -07:00
committed by Facebook GitHub Bot
parent d03b5dca74
commit 652c74104a
2 changed files with 102 additions and 9 deletions
@@ -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();
@@ -176,14 +176,7 @@ export default class Device {
getPagesList(): $ReadOnlyArray<Page> {
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.