Skip to content

Commit

Permalink
fix(dev-middleware): create custom message handler for synthetic page (
Browse files Browse the repository at this point in the history
…#43559)

Summary:
This is a follow-up bugfix for expo/expo#27425, related to:
 - #43291
 - #43307
 - #43310
 - #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: #43559

Test Plan: See added test case.

Reviewed By: motiz88

Differential Revision: D55129412

Pulled By: huntie

fbshipit-source-id: 9679d8fe68f3cb4104f4a042f93612b995baddc9
  • Loading branch information
byCedric authored and facebook-github-bot committed Mar 20, 2024
1 parent d03b5dc commit 652c741
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
27 changes: 18 additions & 9 deletions packages/dev-middleware/src/inspector-proxy/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 652c741

Please sign in to comment.