Skip to content

Commit f8b4c34

Browse files
author
Brian Vaughn
committed
Added tests and fixed edge cases for DevTools iterator handling
1 parent f7052e8 commit f8b4c34

File tree

6 files changed

+125
-6
lines changed

6 files changed

+125
-6
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ exports[`InspectedElementContext should inspect the currently selected element:
200200
}
201201
`;
202202

203+
exports[`InspectedElementContext should not consume iterables while inspecting: 1: Inspected element 2 1`] = `
204+
{
205+
"id": 2,
206+
"owners": null,
207+
"context": null,
208+
"hooks": null,
209+
"props": {
210+
"prop": {}
211+
},
212+
"state": null
213+
}
214+
`;
215+
203216
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
204217
{
205218
"id": 2,

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,57 @@ describe('InspectedElementContext', () => {
789789
done();
790790
});
791791

792+
it('should not consume iterables while inspecting', async done => {
793+
const Example = () => null;
794+
795+
function* generator() {
796+
throw Error('Should not be consumed!');
797+
}
798+
799+
const container = document.createElement('div');
800+
801+
const iterable = generator();
802+
await utils.actAsync(() =>
803+
ReactDOM.render(<Example prop={iterable} />, container),
804+
);
805+
806+
const id = ((store.getElementIDAtIndex(0): any): number);
807+
808+
let inspectedElement = null;
809+
810+
function Suspender({target}) {
811+
const {getInspectedElement} = React.useContext(InspectedElementContext);
812+
inspectedElement = getInspectedElement(id);
813+
return null;
814+
}
815+
816+
await utils.actAsync(
817+
() =>
818+
TestRenderer.create(
819+
<Contexts
820+
defaultSelectedElementID={id}
821+
defaultSelectedElementIndex={0}>
822+
<React.Suspense fallback={null}>
823+
<Suspender target={id} />
824+
</React.Suspense>
825+
</Contexts>,
826+
),
827+
false,
828+
);
829+
830+
expect(inspectedElement).not.toBeNull();
831+
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
832+
833+
const {prop} = (inspectedElement: any).props;
834+
expect(prop[meta.inspectable]).toBe(false);
835+
expect(prop[meta.name]).toBe('Generator');
836+
expect(prop[meta.type]).toBe('opaque_iterator');
837+
expect(prop[meta.preview_long]).toBe('Generator');
838+
expect(prop[meta.preview_short]).toBe('Generator');
839+
840+
done();
841+
});
842+
792843
it('should support objects with no prototype', async done => {
793844
const Example = () => null;
794845

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ Object {
1818
}
1919
`;
2020

21+
exports[`InspectedElementContext should not consume iterables while inspecting: 1: Initial inspection 1`] = `
22+
Object {
23+
"id": 2,
24+
"type": "full-data",
25+
"value": {
26+
"id": 2,
27+
"owners": null,
28+
"context": {},
29+
"hooks": null,
30+
"props": {
31+
"iteratable": {}
32+
},
33+
"state": null
34+
},
35+
}
36+
`;
37+
2138
exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
2239
Object {
2340
"id": 2,

packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,36 @@ describe('InspectedElementContext', () => {
397397
done();
398398
});
399399

400+
it('should not consume iterables while inspecting', async done => {
401+
const Example = () => null;
402+
403+
function* generator() {
404+
yield 1;
405+
yield 2;
406+
}
407+
408+
const iteratable = generator();
409+
410+
act(() =>
411+
ReactDOM.render(
412+
<Example iteratable={iteratable} />,
413+
document.createElement('div'),
414+
),
415+
);
416+
417+
const id = ((store.getElementIDAtIndex(0): any): number);
418+
const inspectedElement = await read(id);
419+
420+
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
421+
422+
// Inspecting should not consume the iterable.
423+
expect(iteratable.next().value).toEqual(1);
424+
expect(iteratable.next().value).toEqual(2);
425+
expect(iteratable.next().value).toBeUndefined();
426+
427+
done();
428+
});
429+
400430
it('should support custom objects with enumerable properties and getters', async done => {
401431
class CustomData {
402432
_number = 42;

packages/react-devtools-shared/src/hydration.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,16 @@ export function dehydrate(
265265
return unserializableValue;
266266
}
267267

268+
case 'opaque_iterator':
269+
cleaned.push(path);
270+
return {
271+
inspectable: false,
272+
preview_short: formatDataForPreview(data, false),
273+
preview_long: formatDataForPreview(data, true),
274+
name: data[Symbol.toStringTag],
275+
type,
276+
};
277+
268278
case 'date':
269279
cleaned.push(path);
270280
return {

packages/react-devtools-shared/src/utils.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,6 @@ export function printOperationsArray(operations: Array<number>) {
199199
throw Error(`Unsupported Bridge operation ${operation}`);
200200
}
201201
}
202-
203-
console.log(logs.join('\n '));
204202
}
205203

206204
export function getDefaultComponentFilters(): Array<ComponentFilter> {
@@ -437,9 +435,9 @@ export function getDataType(data: Object): DataType {
437435
// but this seems kind of awkward and expensive.
438436
return 'array_buffer';
439437
} else if (typeof data[Symbol.iterator] === 'function') {
440-
return 'iterator';
441-
} else if (data[Symbol.iterator] === 'data') {
442-
return 'opaque_iterator';
438+
return data[Symbol.iterator]() === data
439+
? 'opaque_iterator'
440+
: 'iterator';
443441
} else if (data.constructor && data.constructor.name === 'RegExp') {
444442
return 'regexp';
445443
} else {
@@ -658,7 +656,7 @@ export function formatDataForPreview(
658656
return `${name}(${data.size})`;
659657
}
660658
case 'opaque_iterator': {
661-
return `${data.constructor.name}(${data.size})`;
659+
return `${data[Symbol.toStringTag]}`;
662660
}
663661
case 'date':
664662
return data.toString();

0 commit comments

Comments
 (0)