mirror of
https://github.com/facebook/react.git
synced 2026-02-23 20:23:02 +00:00
## Summary - While rolling out RDT 5.2.0 on Fusebox, we've discovered that context menus don't work well with this environment. The reason for it is the context menu state implementation - in a global context we define a map of registered context menus, basically what is shown at the moment (see deleted Contexts.js file). These maps are not invalidated on each re-initialization of DevTools frontend, since the bundle (react-devtools-fusebox module) is not reloaded, and this results into RDT throwing an error that some context menu was already registered. - We should not keep such data in a global state, since there is no guarantee that this will be invalidated with each re-initialization of DevTools (like with browser extension, for example). - The new implementation is based on a `ContextMenuContainer` component, which will add all required `contextmenu` event listeners to the anchor-element. This component will also receive a list of `items` that will be displayed in the shown context menu. - The `ContextMenuContainer` component is also using `useImperativeHandle` hook to extend the instance of the component, so context menus can be managed imperatively via `ref`: `contextMenu.current?.hide()`, for example. - **Changed**: The option for copying value to clipboard is now hidden for functions. The reasons for it are: - It is broken in the current implementation, because we call `JSON.stringify` on the value, see `packages/react-devtools-shared/src/backend/utils.js`. - I don't see any reasonable value in doing this for the user, since `Go to definition` option is available and you can inspect the real code and then copy it. - We already filter out fields from objects, if their value is a function, because the whole object is passed to `JSON.stringify`. ## How did you test this change? ### Works with element props and hooks: - All context menu items work reliably for props items - All context menu items work reliably or hooks items https://github.com/facebook/react/assets/28902667/5e2d58b0-92fa-4624-ad1e-2bbd7f12678f ### Works with timeline profiler: - All context menu items work reliably: copying, zooming, ... - Context menu automatically closes on the scroll event https://github.com/facebook/react/assets/28902667/de744cd0-372a-402a-9fa0-743857048d24 ### Works with Fusebox: - Produces no errors - Copy to clipboard context menu item works reliably https://github.com/facebook/react/assets/28902667/0288f5bf-0d44-435c-8842-6b57bc8a7a24
273 lines
8.7 KiB
JavaScript
273 lines
8.7 KiB
JavaScript
/** @flow */
|
|
|
|
'use strict';
|
|
|
|
const {runOnlyForReactRange} = require('./utils');
|
|
const listAppUtils = require('./list-app-utils');
|
|
const devToolsUtils = require('./devtools-utils');
|
|
const {test, expect} = require('@playwright/test');
|
|
const config = require('../../playwright.config');
|
|
const semver = require('semver');
|
|
|
|
test.use(config);
|
|
test.describe('Components', () => {
|
|
let page;
|
|
|
|
test.beforeEach(async ({browser}) => {
|
|
page = await browser.newPage();
|
|
|
|
await page.goto(config.use.url, {
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
|
|
await page.waitForSelector('#iframe');
|
|
|
|
await devToolsUtils.clickButton(page, 'TabBarButton-components');
|
|
});
|
|
|
|
test('Should display initial React components', async () => {
|
|
const appRowCount = await page.evaluate(() => {
|
|
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
|
|
const container = document.getElementById('iframe').contentDocument;
|
|
const rows = findAllNodes(container, [
|
|
createTestNameSelector('ListItem'),
|
|
]);
|
|
return rows.length;
|
|
});
|
|
expect(appRowCount).toBe(3);
|
|
|
|
const devToolsRowCount = await devToolsUtils.getElementCount(
|
|
page,
|
|
'ListItem'
|
|
);
|
|
expect(devToolsRowCount).toBe(3);
|
|
});
|
|
|
|
test('Should display newly added React components', async () => {
|
|
await listAppUtils.addItem(page, 'four');
|
|
|
|
const count = await devToolsUtils.getElementCount(page, 'ListItem');
|
|
expect(count).toBe(4);
|
|
});
|
|
|
|
test('Should allow elements to be inspected', async () => {
|
|
// Select the first list item in DevTools.
|
|
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
|
|
|
|
// Prop names/values may not be editable based on the React version.
|
|
// If they're not editable, make sure they degrade gracefully
|
|
const isEditableName = semver.gte(config.use.react_version, '17.0.0');
|
|
const isEditableValue = semver.gte(config.use.react_version, '16.8.0');
|
|
|
|
// Then read the inspected values.
|
|
const {
|
|
name: propName,
|
|
value: propValue,
|
|
existingNameElementsSize,
|
|
existingValueElementsSize,
|
|
} = await page.evaluate(
|
|
isEditable => {
|
|
const {createTestNameSelector, findAllNodes} =
|
|
window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
// Get name of first prop
|
|
const nameSelector = isEditable.name
|
|
? 'EditableName'
|
|
: 'NonEditableName';
|
|
// Get value of first prop
|
|
const valueSelector = isEditable.value
|
|
? 'EditableValue'
|
|
: 'NonEditableValue';
|
|
|
|
const existingNameElements = findAllNodes(container, [
|
|
createTestNameSelector('InspectedElementPropsTree'),
|
|
createTestNameSelector('KeyValue'),
|
|
createTestNameSelector(nameSelector),
|
|
]);
|
|
const existingValueElements = findAllNodes(container, [
|
|
createTestNameSelector('InspectedElementPropsTree'),
|
|
createTestNameSelector('KeyValue'),
|
|
createTestNameSelector(valueSelector),
|
|
]);
|
|
|
|
const name = isEditable.name
|
|
? existingNameElements[0].value
|
|
: existingNameElements[0].innerText;
|
|
const value = isEditable.value
|
|
? existingValueElements[0].value
|
|
: existingValueElements[0].innerText;
|
|
|
|
return {
|
|
name,
|
|
value,
|
|
existingNameElementsSize: existingNameElements.length,
|
|
existingValueElementsSize: existingValueElements.length,
|
|
};
|
|
},
|
|
{name: isEditableName, value: isEditableValue}
|
|
);
|
|
|
|
expect(existingNameElementsSize).toBe(1);
|
|
expect(existingValueElementsSize).toBe(1);
|
|
expect(propName).toBe('label');
|
|
expect(propValue).toBe('"one"');
|
|
});
|
|
|
|
test('Should allow inspecting source of the element', async () => {
|
|
// Source inspection is available only in modern renderer.
|
|
runOnlyForReactRange('>=16.8');
|
|
|
|
// Select the first list item in DevTools.
|
|
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
|
|
|
|
// Then read the inspected values.
|
|
const sourceText = await page.evaluate(() => {
|
|
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
const source = findAllNodes(container, [
|
|
createTestNameSelector('InspectedElementView-Source'),
|
|
])[0];
|
|
|
|
return source.innerText;
|
|
});
|
|
|
|
// If React version is specified, the e2e-regression.html page will be used
|
|
// If not, then e2e.html, see playwright.config.js, how url is constructed
|
|
expect(sourceText).toMatch(/e2e-app[\-a-zA-Z]*\.js/);
|
|
});
|
|
|
|
test('should allow props to be edited', async () => {
|
|
runOnlyForReactRange('>=16.8');
|
|
|
|
// Select the first list item in DevTools.
|
|
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
|
|
|
|
// Then edit the label prop.
|
|
await page.evaluate(() => {
|
|
const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
focusWithin(container, [
|
|
createTestNameSelector('InspectedElementPropsTree'),
|
|
createTestNameSelector('KeyValue'),
|
|
createTestNameSelector('EditableValue'),
|
|
]);
|
|
});
|
|
|
|
page.keyboard.press('Backspace'); // "
|
|
page.keyboard.press('Backspace'); // e
|
|
page.keyboard.press('Backspace'); // n
|
|
page.keyboard.press('Backspace'); // o
|
|
page.keyboard.insertText('new"');
|
|
page.keyboard.press('Enter');
|
|
|
|
await page.waitForFunction(() => {
|
|
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
|
|
const container = document.getElementById('iframe').contentDocument;
|
|
const rows = findAllNodes(container, [
|
|
createTestNameSelector('ListItem'),
|
|
])[0];
|
|
return rows.innerText === 'new';
|
|
});
|
|
});
|
|
|
|
test('should load and parse hook names for the inspected element', async () => {
|
|
runOnlyForReactRange('>=16.8');
|
|
|
|
// Select the List component DevTools.
|
|
await devToolsUtils.selectElement(page, 'List', 'App');
|
|
|
|
// Then click to load and parse hook names.
|
|
await devToolsUtils.clickButton(page, 'LoadHookNamesButton');
|
|
|
|
// Make sure the expected hook names are parsed and displayed eventually.
|
|
await page.waitForFunction(
|
|
hookNames => {
|
|
const {createTestNameSelector, findAllNodes} =
|
|
window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
const hooksTree = findAllNodes(container, [
|
|
createTestNameSelector('InspectedElementHooksTree'),
|
|
])[0];
|
|
|
|
if (!hooksTree) {
|
|
return false;
|
|
}
|
|
|
|
const hooksTreeText = hooksTree.innerText;
|
|
|
|
for (let i = 0; i < hookNames.length; i++) {
|
|
if (!hooksTreeText.includes(hookNames[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
['State(items)', 'Ref(inputRef)']
|
|
);
|
|
});
|
|
|
|
test('should allow searching for component by name', async () => {
|
|
async function getComponentSearchResultsCount() {
|
|
return await page.evaluate(() => {
|
|
const {createTestNameSelector, findAllNodes} =
|
|
window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
const element = findAllNodes(container, [
|
|
createTestNameSelector('ComponentSearchInput-ResultsCount'),
|
|
])[0];
|
|
return element.innerText;
|
|
});
|
|
}
|
|
|
|
async function focusComponentSearch() {
|
|
await page.evaluate(() => {
|
|
const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
|
|
const container = document.getElementById('devtools');
|
|
|
|
focusWithin(container, [
|
|
createTestNameSelector('ComponentSearchInput-Input'),
|
|
]);
|
|
});
|
|
}
|
|
|
|
await focusComponentSearch();
|
|
page.keyboard.insertText('List');
|
|
let count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('1 | 4');
|
|
|
|
page.keyboard.insertText('Item');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('1 | 3');
|
|
|
|
page.keyboard.press('Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('2 | 3');
|
|
|
|
page.keyboard.press('Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('3 | 3');
|
|
|
|
page.keyboard.press('Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('1 | 3');
|
|
|
|
page.keyboard.press('Shift+Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('3 | 3');
|
|
|
|
page.keyboard.press('Shift+Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('2 | 3');
|
|
|
|
page.keyboard.press('Shift+Enter');
|
|
count = await getComponentSearchResultsCount();
|
|
expect(count).toBe('1 | 3');
|
|
});
|
|
});
|