Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools context menu #17608

Merged
merged 10 commits into from
Dec 18, 2019
62 changes: 52 additions & 10 deletions packages/react-devtools-extensions/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import {createElement} from 'react';
import {createRoot, flushSync} from 'react-dom';
import Bridge from 'react-devtools-shared/src/bridge';
import Store from 'react-devtools-shared/src/devtools/store';
import {
createViewElementSource,
getBrowserName,
getBrowserTheme,
} from './utils';
import {getBrowserName, getBrowserTheme} from './utils';
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
import {
getSavedComponentFilters,
Expand Down Expand Up @@ -155,10 +151,54 @@ function createPanelIfReactLoaded() {
},
);

const viewElementSourceFunction = createViewElementSource(
bridge,
store,
);
const viewAttributeSourceFunction = (id, path) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
// Ask the renderer interface to find the specified attribute,
// and store it as a global variable on the window.
bridge.send('viewAttributeSource', {id, path, rendererID});

setTimeout(() => {
// Ask Chrome to display the location of the attribute,
// assuming the renderer found a match.
chrome.devtools.inspectedWindow.eval(`
if (window.$attribute != null) {
inspect(window.$attribute);
}
`);
}, 100);
}
};

const viewElementSourceFunction = id => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
// Ask the renderer interface to determine the component function,
// and store it as a global variable on the window
bridge.send('viewElementSource', {id, rendererID});

setTimeout(() => {
// Ask Chrome to display the location of the component function,
// or a render method if it is a Class (ideally Class instance, not type)
// assuming the renderer found one.
chrome.devtools.inspectedWindow.eval(`
if (window.$type != null) {
if (
window.$type &&
window.$type.prototype &&
window.$type.prototype.isReactComponent
) {
// inspect Component.render, not constructor
inspect(window.$type.prototype.render);
} else {
// inspect Functional Component
inspect(window.$type);
}
}
`);
}, 100);
}
};

root = createRoot(document.createElement('div'));

Expand All @@ -170,11 +210,13 @@ function createPanelIfReactLoaded() {
bridge,
browserTheme: getBrowserTheme(),
componentsPortalContainer,
enabledInspectedElementContextMenu: true,
overrideTab,
profilerPortalContainer,
showTabBar: false,
warnIfUnsupportedVersionDetected: true,
store,
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
viewElementSourceFunction,
}),
);
Expand Down
32 changes: 0 additions & 32 deletions packages/react-devtools-extensions/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,6 @@

const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;

export function createViewElementSource(bridge: Bridge, store: Store) {
return function viewElementSource(id) {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
// Ask the renderer interface to determine the component function,
// and store it as a global variable on the window
bridge.send('viewElementSource', {id, rendererID});

setTimeout(() => {
// Ask Chrome to display the location of the component function,
// or a render method if it is a Class (ideally Class instance, not type)
// assuming the renderer found one.
chrome.devtools.inspectedWindow.eval(`
if (window.$type != null) {
if (
window.$type &&
window.$type.prototype &&
window.$type.prototype.isReactComponent
) {
// inspect Component.render, not constructor
inspect(window.$type.prototype.render);
} else {
// inspect Functional Component
inspect(window.$type);
}
}
`);
}, 100);
}
};
}

export type BrowserName = 'Chrome' | 'Firefox';

export function getBrowserName(): BrowserName {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
*/

import typeof ReactTestRenderer from 'react-test-renderer';
import type {GetInspectedElementPath} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
import type {
CopyInspectedElementPath,
GetInspectedElementPath,
StoreAsGlobal,
} from 'react-devtools-shared/src/devtools/views/Components/InspectedElementContext';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type Store from 'react-devtools-shared/src/devtools/store';

Expand Down Expand Up @@ -1203,4 +1207,139 @@ describe('InspectedElementContext', () => {

done();
});

it('should enable inspected values to be stored as global variables', async done => {
const Example = () => null;

const nestedObject = {
a: {
value: 1,
b: {
value: 1,
c: {
value: 1,
},
},
},
};

await utils.actAsync(() =>
ReactDOM.render(
<Example nestedObject={nestedObject} />,
document.createElement('div'),
),
);

const id = ((store.getElementIDAtIndex(0): any): number);

let storeAsGlobal: StoreAsGlobal = ((null: any): StoreAsGlobal);

function Suspender({target}) {
const context = React.useContext(InspectedElementContext);
storeAsGlobal = context.storeAsGlobal;
return null;
}

await utils.actAsync(
() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={0}>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>,
),
false,
);
expect(storeAsGlobal).not.toBeNull();

const logSpy = jest.fn();
spyOn(console, 'log').and.callFake(logSpy);

// Should store the whole value (not just the hydrated parts)
storeAsGlobal(id, ['props', 'nestedObject']);
jest.runOnlyPendingTimers();
expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
expect(global.$reactTemp1).toBe(nestedObject);

logSpy.mockReset();

// Should store the nested property specified (not just the outer value)
storeAsGlobal(id, ['props', 'nestedObject', 'a', 'b']);
jest.runOnlyPendingTimers();
expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
expect(global.$reactTemp2).toBe(nestedObject.a.b);

done();
});

it('should enable inspected values to be copied to the clipboard', async done => {
const Example = () => null;

const nestedObject = {
a: {
value: 1,
b: {
value: 1,
c: {
value: 1,
},
},
},
};

await utils.actAsync(() =>
ReactDOM.render(
<Example nestedObject={nestedObject} />,
document.createElement('div'),
),
);

const id = ((store.getElementIDAtIndex(0): any): number);

let copyPath: CopyInspectedElementPath = ((null: any): CopyInspectedElementPath);

function Suspender({target}) {
const context = React.useContext(InspectedElementContext);
copyPath = context.copyInspectedElementPath;
return null;
}

await utils.actAsync(
() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={0}>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>,
),
false,
);
expect(copyPath).not.toBeNull();

// Should copy the whole value (not just the hydrated parts)
copyPath(id, ['props', 'nestedObject']);
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject),
);

global.mockClipboardCopy.mockReset();

// Should copy the nested property specified (not just the outer value)
copyPath(id, ['props', 'nestedObject', 'a', 'b']);
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject.a.b),
);

done();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -392,4 +392,109 @@ describe('InspectedElementContext', () => {

done();
});

it('should enable inspected values to be stored as global variables', () => {
const Example = () => null;

const nestedObject = {
a: {
value: 1,
b: {
value: 1,
c: {
value: 1,
},
},
},
};

act(() =>
ReactDOM.render(
<Example nestedObject={nestedObject} />,
document.createElement('div'),
),
);

const id = ((store.getElementIDAtIndex(0): any): number);
const rendererID = ((store.getRendererIDForElement(id): any): number);

const logSpy = jest.fn();
spyOn(console, 'log').and.callFake(logSpy);

// Should store the whole value (not just the hydrated parts)
bridge.send('storeAsGlobal', {
count: 1,
id,
path: ['props', 'nestedObject'],
rendererID,
});
jest.runOnlyPendingTimers();
expect(logSpy).toHaveBeenCalledWith('$reactTemp1');
expect(global.$reactTemp1).toBe(nestedObject);

logSpy.mockReset();

// Should store the nested property specified (not just the outer value)
bridge.send('storeAsGlobal', {
count: 2,
id,
path: ['props', 'nestedObject', 'a', 'b'],
rendererID,
});
jest.runOnlyPendingTimers();
expect(logSpy).toHaveBeenCalledWith('$reactTemp2');
expect(global.$reactTemp2).toBe(nestedObject.a.b);
});

it('should enable inspected values to be copied to the clipboard', () => {
const Example = () => null;

const nestedObject = {
a: {
value: 1,
b: {
value: 1,
c: {
value: 1,
},
},
},
};

act(() =>
ReactDOM.render(
<Example nestedObject={nestedObject} />,
document.createElement('div'),
),
);

const id = ((store.getElementIDAtIndex(0): any): number);
const rendererID = ((store.getRendererIDForElement(id): any): number);

// Should copy the whole value (not just the hydrated parts)
bridge.send('copyElementPath', {
id,
path: ['props', 'nestedObject'],
rendererID,
});
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject),
);

global.mockClipboardCopy.mockReset();

// Should copy the nested property specified (not just the outer value)
bridge.send('copyElementPath', {
id,
path: ['props', 'nestedObject', 'a', 'b'],
rendererID,
});
jest.runOnlyPendingTimers();
expect(global.mockClipboardCopy).toHaveBeenCalledTimes(1);
expect(global.mockClipboardCopy).toHaveBeenCalledWith(
JSON.stringify(nestedObject.a.b),
);
});
});
Loading