From b74f3f507a0243a124a041f3d5ebc246a98911f4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 10 May 2019 09:13:54 -0700 Subject: [PATCH] Added basic tests for InspectedElementContext --- package.json | 1 + .../inspectedElementContext-test.js.snap | 61 +++++++ src/__tests__/inspectedElementContext-test.js | 162 ++++++++++++++++++ src/__tests__/inspectedElementSerializer.js | 27 +++ src/__tests__/utils.js | 4 +- src/backend/agent.js | 1 - src/devtools/views/Components/TreeContext.js | 15 +- 7 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/__snapshots__/inspectedElementContext-test.js.snap create mode 100644 src/__tests__/inspectedElementContext-test.js create mode 100644 src/__tests__/inspectedElementSerializer.js diff --git a/package.json b/package.json index 3958ceb4bd..3834cacd63 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "/src/__tests__/setupTests" ], "snapshotSerializers": [ + "/src/__tests__/inspectedElementSerializer", "/src/__tests__/storeSerializer" ], "testMatch": [ diff --git a/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap new file mode 100644 index 0000000000..282c866805 --- /dev/null +++ b/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InspectedElementContext should inspect the currently selected element: 1: mount 1`] = ` +[root] + +`; + +exports[`InspectedElementContext should inspect the currently selected element: 2: Inspected element 2 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": [ + { + "id": 0, + "isStateEditable": true, + "name": "State", + "value": 1, + "subHooks": [] + } + ], + "props": { + "foo": 1, + "bar": "abc" + }, + "state": null +} +`; + +exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = ` +[root] + +`; + +exports[`InspectedElementContext should poll for updates for the currently selected element: 2: initial render 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": null, + "props": { + "foo": 1, + "bar": "abc" + }, + "state": null +} +`; + +exports[`InspectedElementContext should poll for updates for the currently selected element: 2: updated state 1`] = ` +{ + "id": 2, + "owners": null, + "context": null, + "hooks": null, + "props": { + "foo": 2, + "bar": "def" + }, + "state": null +} +`; diff --git a/src/__tests__/inspectedElementContext-test.js b/src/__tests__/inspectedElementContext-test.js new file mode 100644 index 0000000000..bf8d7f8bf7 --- /dev/null +++ b/src/__tests__/inspectedElementContext-test.js @@ -0,0 +1,162 @@ +// @flow + +import typeof ReactTestRenderer from 'react-test-renderer'; +import type { Element } from 'src/devtools/views/Components/types'; +import type Bridge from 'src/bridge'; +import type Store from 'src/devtools/store'; + +describe('InspectedElementContext', () => { + let React; + let ReactDOM; + let TestRenderer: ReactTestRenderer; + let bridge: Bridge; + let store: Store; + let utils; + + let BridgeContext; + let InspectedElementContext; + let InspectedElementContextController; + let StoreContext; + let TreeContextController; + + beforeEach(() => { + utils = require('./utils'); + utils.beforeEachProfiling(); + + bridge = global.bridge; + store = global.store; + store.collapseNodesByDefault = false; + + React = require('react'); + ReactDOM = require('react-dom'); + TestRenderer = utils.requireTestRenderer(); + + BridgeContext = require('src/devtools/views/context').BridgeContext; + InspectedElementContext = require('src/devtools/views/Components/InspectedElementContext') + .InspectedElementContext; + InspectedElementContextController = require('src/devtools/views/Components/InspectedElementContext') + .InspectedElementContextController; + StoreContext = require('src/devtools/views/context').StoreContext; + TreeContextController = require('src/devtools/views/Components/TreeContext') + .TreeContextController; + }); + + const Contexts = ({ + children, + defaultSelectedElementID = null, + defaultSelectedElementIndex = null, + }) => ( + + + + + {children} + + + + + ); + + it('should inspect the currently selected element', async done => { + const Example = () => { + const [count] = React.useState(1); + return count; + }; + + const container = document.createElement('div'); + utils.act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('1: mount'); + + const example = ((store.getElementAtIndex(0): any): Element); + + let didFinish = false; + + function Suspender({ target }) { + const { read } = React.useContext(InspectedElementContext); + const inspectedElement = read(target.id); + expect(inspectedElement).toMatchSnapshot( + `2: Inspected element ${target.id}` + ); + didFinish = true; + return null; + } + + await utils.actAsync( + () => + TestRenderer.create( + + + + + + ), + 3 + ); + expect(didFinish).toBe(true); + + done(); + }); + + it('should poll for updates for the currently selected element', async done => { + const Example = () => null; + + const container = document.createElement('div'); + utils.act(() => ReactDOM.render(, container)); + expect(store).toMatchSnapshot('1: mount'); + + const example = ((store.getElementAtIndex(0): any): Element); + + let inspectedElement = null; + + function Suspender({ target }) { + const { read } = React.useContext(InspectedElementContext); + inspectedElement = read(target.id); + return null; + } + + await utils.actAsync( + () => + TestRenderer.create( + + + + + + ), + 3 + ); + expect(inspectedElement).toMatchSnapshot('2: initial render'); + + await utils.actAsync(() => + ReactDOM.render(, container) + ); + + inspectedElement = null; + await utils.actAsync( + () => + TestRenderer.create( + + + + + + ), + 1 + ); + expect(inspectedElement).toMatchSnapshot('2: updated state'); + + done(); + }); +}); diff --git a/src/__tests__/inspectedElementSerializer.js b/src/__tests__/inspectedElementSerializer.js new file mode 100644 index 0000000000..2423683c24 --- /dev/null +++ b/src/__tests__/inspectedElementSerializer.js @@ -0,0 +1,27 @@ +// test() is part of Jest's serializer API +export function test(maybeInspectedElement) { + return ( + maybeInspectedElement !== null && + typeof maybeInspectedElement === 'object' && + maybeInspectedElement.hasOwnProperty('canEditFunctionProps') && + maybeInspectedElement.hasOwnProperty('canEditHooks') && + maybeInspectedElement.hasOwnProperty('canToggleSuspense') && + maybeInspectedElement.hasOwnProperty('canViewSource') + ); +} + +// print() is part of Jest's serializer API +export function print(inspectedElement, serialize, indent) { + return JSON.stringify( + { + id: inspectedElement.id, + owners: inspectedElement.owners, + context: inspectedElement.context, + hooks: inspectedElement.hooks, + props: inspectedElement.props, + state: inspectedElement.state, + }, + null, + 2 + ); +} diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index fcbf923624..cc7050575d 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -26,7 +26,7 @@ export async function actAsync( callback(); // Resolve pending suspense promises - jest.runAllTimers(); + jest.runOnlyPendingTimers(); }); // Run cascading microtasks and flush scheduled React work. @@ -35,7 +35,7 @@ export async function actAsync( while (--numTimesToFlush >= 0) { // $FlowFixMe Flow doens't know about "await act()" yet await TestUtils.act(async () => { - jest.runAllTimers(); + jest.runOnlyPendingTimers(); Scheduler.flushAll(); }); } diff --git a/src/backend/agent.js b/src/backend/agent.js index 0c8fd77f09..d675db7c78 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -358,7 +358,6 @@ export default class Agent extends EventEmitter { console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`); } else { renderer.selectElement(id); - this._bridge.send('selectElement'); // When user selects an element, stop trying to restore the selection, // and instead remember the current selection for the next reload. diff --git a/src/devtools/views/Components/TreeContext.js b/src/devtools/views/Components/TreeContext.js index 067f8fbaf4..90c514b3e7 100644 --- a/src/devtools/views/Components/TreeContext.js +++ b/src/devtools/views/Components/TreeContext.js @@ -594,10 +594,17 @@ type Props = {| // Used for automated testing defaultOwnerID?: ?number, + defaultSelectedElementID?: ?number, + defaultSelectedElementIndex?: ?number, |}; // TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists. -function TreeContextController({ children, defaultOwnerID }: Props) { +function TreeContextController({ + children, + defaultOwnerID, + defaultSelectedElementID, + defaultSelectedElementIndex, +}: Props) { const bridge = useContext(BridgeContext); const store = useContext(StoreContext); @@ -652,8 +659,10 @@ function TreeContextController({ children, defaultOwnerID }: Props) { const [state, dispatch] = useReducer(reducer, { // Tree numElements: store.numElements, - selectedElementIndex: null, - selectedElementID: null, + selectedElementID: + defaultSelectedElementID == null ? null : defaultSelectedElementID, + selectedElementIndex: + defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex, // Search searchIndex: null,