Skip to content

Commit 113b5f5

Browse files
author
Brian Vaughn
committed
Added basic editable props/state/context/hooks tests
1 parent 36df483 commit 113b5f5

File tree

4 files changed

+255
-37
lines changed

4 files changed

+255
-37
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`InspectedElementContext display complex values of useDebugValue: DisplayedComplexValue 1`] = `
4-
{
5-
"id": 2,
6-
"owners": null,
7-
"context": null,
8-
"hooks": [
9-
{
10-
"id": null,
11-
"isStateEditable": false,
12-
"name": "DebuggableHook",
13-
"value": {
14-
"foo": 2
15-
},
16-
"subHooks": [
17-
{
18-
"id": 0,
19-
"isStateEditable": true,
20-
"name": "State",
21-
"value": 1,
22-
"subHooks": []
23-
}
24-
]
25-
}
26-
],
27-
"props": {},
28-
"state": null
29-
}
30-
`;
31-
323
exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
334
{
345
"id": 2,
@@ -65,6 +36,35 @@ exports[`InspectedElementContext should dehydrate complex nested values when req
6536
}
6637
`;
6738

39+
exports[`InspectedElementContext should display complex values of useDebugValue: DisplayedComplexValue 1`] = `
40+
{
41+
"id": 2,
42+
"owners": null,
43+
"context": null,
44+
"hooks": [
45+
{
46+
"id": null,
47+
"isStateEditable": false,
48+
"name": "DebuggableHook",
49+
"value": {
50+
"foo": 2
51+
},
52+
"subHooks": [
53+
{
54+
"id": 0,
55+
"isStateEditable": true,
56+
"name": "State",
57+
"value": 1,
58+
"subHooks": []
59+
}
60+
]
61+
}
62+
],
63+
"props": {},
64+
"state": null
65+
}
66+
`;
67+
6868
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
6969
{
7070
"id": 2,
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
11+
import type Store from 'react-devtools-shared/src/devtools/store';
12+
13+
describe('editable props and state', () => {
14+
let PropTypes;
15+
let React;
16+
let ReactDOM;
17+
let bridge: FrontendBridge;
18+
let store: Store;
19+
let utils;
20+
21+
beforeEach(() => {
22+
utils = require('./utils');
23+
utils.beforeEachProfiling();
24+
25+
bridge = global.bridge;
26+
store = global.store;
27+
store.collapseNodesByDefault = false;
28+
29+
PropTypes = require('prop-types');
30+
React = require('react');
31+
ReactDOM = require('react-dom');
32+
});
33+
34+
it('should support editing props', async () => {
35+
class ClassComponent extends React.Component {
36+
render() {
37+
return this.props.deep.nested + this.props.shallow;
38+
}
39+
}
40+
41+
function FunctionComponent(props) {
42+
return props.deep.nested + props.shallow;
43+
}
44+
45+
const container = document.createElement('div');
46+
await utils.actAsync(() =>
47+
ReactDOM.render(
48+
<>
49+
<ClassComponent deep={{nested: 'a'}} shallow="b" />,
50+
<FunctionComponent deep={{nested: 'a'}} shallow="b" />,
51+
</>,
52+
container,
53+
),
54+
);
55+
56+
expect(container.innerHTML).toBe('ab,ab,');
57+
58+
function overrideProps(id, path, value) {
59+
const rendererID = utils.getRendererID();
60+
bridge.send('overrideProps', {id, path, rendererID, value});
61+
jest.runOnlyPendingTimers();
62+
}
63+
64+
const classID = ((store.getElementIDAtIndex(0): any): number);
65+
const funcID = ((store.getElementIDAtIndex(1): any): number);
66+
67+
overrideProps(classID, ['shallow'], 'c');
68+
expect(container.innerHTML).toBe('ac,ab,');
69+
70+
overrideProps(classID, ['deep', 'nested'], 'd');
71+
expect(container.innerHTML).toBe('dc,ab,');
72+
73+
overrideProps(funcID, ['shallow'], 'c');
74+
expect(container.innerHTML).toBe('dc,ac,');
75+
76+
overrideProps(funcID, ['deep', 'nested'], 'd');
77+
expect(container.innerHTML).toBe('dc,dc,');
78+
});
79+
80+
it('should support editing state', async () => {
81+
class ClassComponent extends React.Component {
82+
state = {
83+
deep: {
84+
nested: 'a',
85+
},
86+
shallow: 'b',
87+
};
88+
render() {
89+
return this.state.deep.nested + this.state.shallow;
90+
}
91+
}
92+
93+
const container = document.createElement('div');
94+
await utils.actAsync(() => ReactDOM.render(<ClassComponent />, container));
95+
96+
expect(container.innerHTML).toBe('ab');
97+
98+
function overrideState(id, path, value) {
99+
const rendererID = utils.getRendererID();
100+
bridge.send('overrideState', {id, path, rendererID, value});
101+
jest.runOnlyPendingTimers();
102+
}
103+
104+
const id = ((store.getElementIDAtIndex(0): any): number);
105+
106+
overrideState(id, ['shallow'], 'c');
107+
expect(container.innerHTML).toBe('ac');
108+
109+
overrideState(id, ['deep', 'nested'], 'd');
110+
expect(container.innerHTML).toBe('dc');
111+
});
112+
113+
it('should support editing hooks', async () => {
114+
function FunctionComponent() {
115+
const [state] = React.useState({
116+
deep: {
117+
nested: 'a',
118+
},
119+
shallow: 'b',
120+
});
121+
return state.deep.nested + state.shallow;
122+
}
123+
124+
const container = document.createElement('div');
125+
await utils.actAsync(() =>
126+
ReactDOM.render(<FunctionComponent />, container),
127+
);
128+
129+
expect(container.innerHTML).toBe('ab');
130+
131+
function overrideHookState(id, hookID, path, value) {
132+
const rendererID = utils.getRendererID();
133+
bridge.send('overrideHookState', {id, hookID, path, rendererID, value});
134+
jest.runOnlyPendingTimers();
135+
}
136+
137+
const id = ((store.getElementIDAtIndex(0): any): number);
138+
const hookID = 0; // index
139+
140+
overrideHookState(id, hookID, ['shallow'], 'c');
141+
expect(container.innerHTML).toBe('ac');
142+
143+
overrideHookState(id, hookID, ['deep', 'nested'], 'd');
144+
expect(container.innerHTML).toBe('dc');
145+
});
146+
147+
it('should support editing context', async () => {
148+
class LegacyContextProvider extends React.Component<any> {
149+
static childContextTypes = {
150+
deep: PropTypes.object,
151+
shallow: PropTypes.string,
152+
};
153+
getChildContext() {
154+
return {
155+
deep: {
156+
nested: 'a',
157+
},
158+
shallow: 'b',
159+
};
160+
}
161+
render() {
162+
return this.props.children;
163+
}
164+
}
165+
166+
class ClassComponent extends React.Component<any> {
167+
static contextTypes = {
168+
deep: PropTypes.object,
169+
shallow: PropTypes.string,
170+
};
171+
172+
render() {
173+
return this.context.deep.nested + this.context.shallow;
174+
}
175+
}
176+
177+
const container = document.createElement('div');
178+
await utils.actAsync(() =>
179+
ReactDOM.render(
180+
<LegacyContextProvider>
181+
<ClassComponent />,
182+
<ClassComponent />,
183+
</LegacyContextProvider>,
184+
container,
185+
),
186+
);
187+
188+
expect(container.innerHTML).toBe('ab,ab,');
189+
190+
function overrideContext(id, path, value) {
191+
const rendererID = utils.getRendererID();
192+
// To simplify hydration and display of primitive context values (e.g. number, string)
193+
// the inspectElement() method wraps context in a {value: ...} object.
194+
path = ['value', ...path];
195+
196+
bridge.send('overrideContext', {id, path, rendererID, value});
197+
jest.runOnlyPendingTimers();
198+
}
199+
200+
const classID = ((store.getElementIDAtIndex(1): any): number);
201+
202+
overrideContext(classID, ['shallow'], 'c');
203+
expect(container.innerHTML).toBe('ac,ab,');
204+
205+
overrideContext(classID, ['deep', 'nested'], 'd');
206+
expect(container.innerHTML).toBe('dc,ab,');
207+
208+
// Function components using legacy context are not editable.
209+
});
210+
});

packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1588,7 +1588,7 @@ describe('InspectedElementContext', () => {
15881588
done();
15891589
});
15901590

1591-
it('display complex values of useDebugValue', async done => {
1591+
it('should display complex values of useDebugValue', async done => {
15921592
let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
15931593
let inspectedElement = null;
15941594
function Suspender({target}) {

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2728,14 +2728,22 @@ export function attach(
27282728

27292729
const fiber = findCurrentFiberUsingSlowPathById(id);
27302730
if (fiber !== null) {
2731-
const instance = fiber.stateNode;
2732-
if (path.length === 0) {
2733-
// Simple context value
2734-
instance.context = value;
2735-
} else {
2736-
setInObject(instance.context, path, value);
2731+
switch (fiber.tag) {
2732+
case ClassComponent:
2733+
const instance = fiber.stateNode;
2734+
if (path.length === 0) {
2735+
// Simple context value
2736+
instance.context = value;
2737+
} else {
2738+
setInObject(instance.context, path, value);
2739+
}
2740+
instance.forceUpdate();
2741+
break;
2742+
case FunctionComponent:
2743+
// Function components using legacy context are not editable
2744+
// because there's no instance on which to create a cloned, mutated context.
2745+
break;
27372746
}
2738-
instance.forceUpdate();
27392747
}
27402748
}
27412749

0 commit comments

Comments
 (0)