chore(ui): update to react 18 (#32079)

Part of https://github.com/microsoft/playwright/issues/31863. Updates
most of our React usage to React 18. `recorder` doesn't seem to like it
yet. I suspect that some of our code isn't compatible with concurrent
mode, i've investigated that in
https://github.com/microsoft/playwright/pull/32101.

---------

Signed-off-by: Simon Knott <info@simonknott.de>
Co-authored-by: Max Schmitt <max@schmitt.mx>
This commit is contained in:
Simon Knott 2024-08-12 13:50:11 +02:00 committed by GitHub
parent a30a8805c9
commit c8cc4f9c8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 23 additions and 20 deletions

View File

@ -19,7 +19,7 @@ import type zip from '@zip.js/zip.js';
// @ts-ignore // @ts-ignore
import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js'; import * as zipImport from '@zip.js/zip.js/lib/zip-no-worker-inflate.js';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom/client';
import './colors.css'; import './colors.css';
import type { LoadedReport } from './loadedReport'; import type { LoadedReport } from './loadedReport';
import { ReportView } from './reportView'; import { ReportView } from './reportView';
@ -44,7 +44,7 @@ const ReportLoader: React.FC = () => {
}; };
window.onload = () => { window.onload = () => {
ReactDOM.render(<ReportLoader />, document.querySelector('#root')); ReactDOM.createRoot(document.querySelector('#root')!).render(<ReportLoader />);
}; };
class ZipReport implements LoadedReport { class ZipReport implements LoadedReport {

View File

@ -17,7 +17,7 @@
import '@web/common.css'; import '@web/common.css';
import { applyTheme } from '@web/theme'; import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css'; import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom/client';
import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
(async () => { (async () => {
@ -56,5 +56,5 @@ import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
setInterval(function() { fetch('ping'); }, 10000); setInterval(function() { fetch('ping'); }, 10000);
} }
ReactDOM.render(<EmbeddedWorkbenchLoader />, document.querySelector('#root')); ReactDOM.createRoot(document.querySelector('#root')!).render(<EmbeddedWorkbenchLoader />);
})(); })();

View File

@ -17,7 +17,7 @@
import '@web/common.css'; import '@web/common.css';
import { applyTheme } from '@web/theme'; import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css'; import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom/client';
import { WorkbenchLoader } from './ui/workbenchLoader'; import { WorkbenchLoader } from './ui/workbenchLoader';
(async () => { (async () => {
@ -38,5 +38,5 @@ import { WorkbenchLoader } from './ui/workbenchLoader';
setInterval(function() { fetch('ping'); }, 10000); setInterval(function() { fetch('ping'); }, 10000);
} }
ReactDOM.render(<WorkbenchLoader/>, document.querySelector('#root')); ReactDOM.createRoot(document.querySelector('#root')!).render(<WorkbenchLoader/>);
})(); })();

View File

@ -40,7 +40,7 @@ export const TestListView: React.FC<{
testServerConnection: TestServerConnection | undefined, testServerConnection: TestServerConnection | undefined,
testModel?: TestModel, testModel?: TestModel,
runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set<string>) => void, runTests: (mode: 'bounce-if-busy' | 'queue-if-busy', testIds: Set<string>) => void,
runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean }, runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean, completed?: boolean },
watchAll: boolean, watchAll: boolean,
watchedTreeIds: { value: Set<string> }, watchedTreeIds: { value: Set<string> },
setWatchedTreeIds: (ids: { value: Set<string> }) => void, setWatchedTreeIds: (ids: { value: Set<string> }) => void,
@ -154,7 +154,7 @@ export const TestListView: React.FC<{
</div> </div>
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>} {!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
<Toolbar noMinHeight={true} noShadow={true}> <Toolbar noMinHeight={true} noShadow={true}>
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton> <ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}></ToolbarButton>
<ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton> <ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => { {!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
if (watchedTreeIds.value.has(treeItem.id)) if (watchedTreeIds.value.has(treeItem.id))

View File

@ -85,7 +85,9 @@ export const UIModeView: React.FC<{}> = ({
const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }>({}); const [selectedItem, setSelectedItem] = React.useState<{ treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase }>({});
const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set()); const [visibleTestIds, setVisibleTestIds] = React.useState<Set<string>>(new Set());
const [isLoading, setIsLoading] = React.useState<boolean>(false); const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean } | undefined>(); const [runningState, setRunningState] = React.useState<{ testIds: Set<string>, itemSelectedByUser?: boolean, completed?: boolean } | undefined>();
const isRunningTest = runningState && !runningState.completed;
const [watchAll, setWatchAll] = useSetting<boolean>('watch-all', false); const [watchAll, setWatchAll] = useSetting<boolean>('watch-all', false);
const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() }); const [watchedTreeIds, setWatchedTreeIds] = React.useState<{ value: Set<string> }>({ value: new Set() });
const commandQueue = React.useRef(Promise.resolve()); const commandQueue = React.useRef(Promise.resolve());
@ -251,29 +253,29 @@ export const UIModeView: React.FC<{}> = ({
// Update progress. // Update progress.
React.useEffect(() => { React.useEffect(() => {
if (runningState && testModel?.progress) if (isRunningTest && testModel?.progress)
setProgress(testModel.progress); setProgress(testModel.progress);
else if (!testModel) else if (!testModel)
setProgress(undefined); setProgress(undefined);
}, [testModel, runningState]); }, [testModel, isRunningTest]);
// Test tree is built from the model and filters. // Test tree is built from the model and filters.
const { testTree } = React.useMemo(() => { const { testTree } = React.useMemo(() => {
if (!testModel) if (!testModel)
return { testTree: new TestTree('', new TeleSuite('', 'root'), [], projectFilters, pathSeparator) }; return { testTree: new TestTree('', new TeleSuite('', 'root'), [], projectFilters, pathSeparator) };
const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, pathSeparator); const testTree = new TestTree('', testModel.rootSuite, testModel.loadErrors, projectFilters, pathSeparator);
testTree.filterTree(filterText, statusFilters, runningState?.testIds); testTree.filterTree(filterText, statusFilters, isRunningTest ? runningState?.testIds : undefined);
testTree.sortAndPropagateStatus(); testTree.sortAndPropagateStatus();
testTree.shortenRoot(); testTree.shortenRoot();
testTree.flattenForSingleProject(); testTree.flattenForSingleProject();
setVisibleTestIds(testTree.testIds()); setVisibleTestIds(testTree.testIds());
return { testTree }; return { testTree };
}, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState]); }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds, runningState, isRunningTest]);
const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => { const runTests = React.useCallback((mode: 'queue-if-busy' | 'bounce-if-busy', testIds: Set<string>) => {
if (!testServerConnection || !testModel) if (!testServerConnection || !testModel)
return; return;
if (mode === 'bounce-if-busy' && runningState) if (mode === 'bounce-if-busy' && isRunningTest)
return; return;
runTestBacklog.current = new Set([...runTestBacklog.current, ...testIds]); runTestBacklog.current = new Set([...runTestBacklog.current, ...testIds]);
@ -320,9 +322,9 @@ export const UIModeView: React.FC<{}> = ({
test.results = []; test.results = [];
} }
setTestModel({ ...testModel }); setTestModel({ ...testModel });
setRunningState(undefined); setRunningState(oldState => oldState ? ({ ...oldState, completed: true }) : undefined);
}); });
}, [projectFilters, runningState, testModel, testServerConnection, runWorkers, runHeaded, runUpdateSnapshots]); }, [projectFilters, isRunningTest, testModel, testServerConnection, runWorkers, runHeaded, runUpdateSnapshots]);
React.useEffect(() => { React.useEffect(() => {
if (!testServerConnection || !teleSuiteUpdater) if (!testServerConnection || !teleSuiteUpdater)
@ -396,7 +398,6 @@ export const UIModeView: React.FC<{}> = ({
}; };
}, [runTests, reloadTests, testServerConnection, visibleTestIds, isShowingOutput]); }, [runTests, reloadTests, testServerConnection, visibleTestIds, isShowingOutput]);
const isRunningTest = !!runningState;
const dialogRef = React.useRef<HTMLDialogElement>(null); const dialogRef = React.useRef<HTMLDialogElement>(null);
const openInstallDialog = React.useCallback((e: React.MouseEvent) => { const openInstallDialog = React.useCallback((e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();

View File

@ -17,7 +17,7 @@
import '@web/common.css'; import '@web/common.css';
import { applyTheme } from '@web/theme'; import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css'; import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom/client';
import { UIModeView } from './ui/uiModeView'; import { UIModeView } from './ui/uiModeView';
(async () => { (async () => {
@ -38,5 +38,5 @@ import { UIModeView } from './ui/uiModeView';
setInterval(function() { fetch('ping'); }, 10000); setInterval(function() { fetch('ping'); }, 10000);
} }
ReactDOM.render(<UIModeView></UIModeView>, document.querySelector('#root')); ReactDOM.createRoot(document.querySelector('#root')!).render(<UIModeView/>);
})(); })();

View File

@ -174,7 +174,9 @@ test('should run by project', async ({ runUITest }) => {
await expect.poll(dumpTestTree(page)).toBe(` await expect.poll(dumpTestTree(page)).toBe(`
a.test.ts a.test.ts
passes passes
fails <= fails
foo <=
bar
suite suite
b.test.ts b.test.ts
passes passes