refactor(ui): some react refactorings (#31900)

Addresses https://github.com/microsoft/playwright/issues/31863. This PR
is chonky, but the individual commits should be easy to review. If
they're not, i'm happy to break them out into individual PRs.

There's two main things this does:

1. Remove some unused imports
2. Add a `clsx`-inspired helper function for classname templating

I wasn't able to replace `ReactDOM.render` with `ReactDOM.createRoot`.
This is the new recommended way starting with React 18, and the existing
one is going to be deprecated at some point. But it somehow breaks our
tests, i'll have to investigate that separately.
This commit is contained in:
Simon Knott 2024-07-31 12:12:06 +02:00 committed by GitHub
parent 64fe245297
commit 99724d0322
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 54 additions and 47 deletions

View File

@ -19,6 +19,7 @@ import './chip.css';
import './colors.css';
import './common.css';
import * as icons from './icons';
import { clsx } from '@web/uiUtils';
export const Chip: React.FC<{
header: JSX.Element | string,
@ -31,14 +32,14 @@ export const Chip: React.FC<{
}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId, targetRef }) => {
return <div className='chip' data-testid={dataTestId} ref={targetRef}>
<div
className={'chip-header' + (setExpanded ? ' expanded-' + expanded : '')}
className={clsx('chip-header', setExpanded && ' expanded-' + expanded)}
onClick={() => setExpanded?.(!expanded)}
title={typeof header === 'string' ? header : undefined}>
{setExpanded && !!expanded && icons.downArrow()}
{setExpanded && !expanded && icons.rightArrow()}
{header}
</div>
{(!setExpanded || expanded) && <div className={'chip-body' + (noInsets ? ' chip-body-no-insets' : '')}>{children}</div>}
{(!setExpanded || expanded) && <div className={clsx('chip-body', noInsets && 'chip-body-no-insets')}>{children}</div>}
</div>;
};

View File

@ -21,6 +21,7 @@ import { TreeItem } from './treeItem';
import { CopyToClipboard } from './copyToClipboard';
import './links.css';
import { linkifyText } from './renderUtils';
import { clsx } from '@web/uiUtils';
export function navigate(href: string) {
window.history.pushState({}, '', href);
@ -48,8 +49,8 @@ export const Link: React.FunctionComponent<{
className?: string,
title?: string,
children: any,
}> = ({ href, click, ctrlClick, className, children, title }) => {
return <a style={{ textDecoration: 'none', color: 'var(--color-fg-default)', cursor: 'pointer' }} href={href} className={`${className || ''}`} title={title} onClick={e => {
}> = ({ click, ctrlClick, children, ...rest }) => {
return <a {...rest} style={{ textDecoration: 'none', color: 'var(--color-fg-default)', cursor: 'pointer' }} onClick={e => {
if (click) {
e.preventDefault();
navigate(e.metaKey || e.ctrlKey ? ctrlClick || click : click);
@ -64,7 +65,7 @@ export const ProjectLink: React.FunctionComponent<{
const encoded = encodeURIComponent(projectName);
const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`;
return <Link href={`#?q=p:${value}`}>
<span className={'label label-color-' + (projectNames.indexOf(projectName) % 6)} style={{ margin: '6px 0 0 6px' }}>
<span className={clsx('label', `label-color-${projectNames.indexOf(projectName) % 6}`)} style={{ margin: '6px 0 0 6px' }}>
{projectName}
</span>
</Link>;

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { clsx } from '@web/uiUtils';
import './tabbedPane.css';
import * as React from 'react';
@ -34,7 +35,7 @@ export const TabbedPane: React.FunctionComponent<{
<div className='hbox' style={{ flex: 'none' }}>
<div className='tabbed-pane-tab-strip'>{
tabs.map(tab => (
<div className={'tabbed-pane-tab-element ' + (selectedTab === tab.id ? 'selected' : '')}
<div className={clsx('tabbed-pane-tab-element', selectedTab === tab.id && 'selected')}
onClick={() => setSelectedTab(tab.id)}
key={tab.id}>
<div className='tabbed-pane-tab-label'>{tab.title}</div>

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import { TestCaseView } from './testCaseView';
import type { TestCase, TestResult } from './types';

View File

@ -25,6 +25,7 @@ import './testCaseView.css';
import { TestResultView } from './testResultView';
import { linkifyText } from './renderUtils';
import { hashStringToInt, msToString } from './utils';
import { clsx } from '@web/uiUtils';
export const TestCaseView: React.FC<{
projectNames: string[],
@ -90,7 +91,7 @@ const LabelsLinkView: React.FC<React.PropsWithChildren<{
<>
{labels.map(label => (
<a key={label} style={{ textDecoration: 'none', color: 'var(--color-fg-default)' }} href={`#?q=${label}`} >
<span style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={'label label-color-' + (hashStringToInt(label))}>
<span style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={clsx('label', 'label-color-' + hashStringToInt(label))}>
{label.slice(1)}
</span>
</a>

View File

@ -23,6 +23,7 @@ import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image, trace } from './icons';
import { clsx } from '@web/uiUtils';
export const TestFileView: React.FC<React.PropsWithChildren<{
report: HTMLReport;
@ -39,7 +40,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
{file.fileName}
</span>}>
{file.tests.filter(t => filter.matches(t)).map(test =>
<div key={`test-${test.testId}`} className={'test-file-test test-file-test-outcome-' + test.outcome}>
<div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
<div className='hbox' style={{ alignItems: 'flex-start' }}>
<div className='hbox'>
<span className='test-file-test-status-icon'>
@ -101,7 +102,7 @@ const LabelsClickView: React.FC<React.PropsWithChildren<{
return labels.length > 0 ? (
<>
{labels.map(label => (
<span key={label} style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={'label label-color-' + (hashStringToInt(label))} onClick={e => onClickHandle(e, label)}>
<span key={label} style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={clsx('label', 'label-color-' + hashStringToInt(label))} onClick={e => onClickHandle(e, label)}>
{label.slice(1)}
</span>
))}

View File

@ -17,7 +17,7 @@
import './callLog.css';
import * as React from 'react';
import type { CallLog } from './recorderTypes';
import { msToString } from '@web/uiUtils';
import { clsx, msToString } from '@web/uiUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
@ -53,9 +53,9 @@ export const CallLogView: React.FC<CallLogProps> = ({
titlePrefix = callLog.title + '(';
titleSuffix = ')';
}
return <div className={`call-log-call ${callLog.status}`} key={callLog.id}>
return <div className={clsx('call-log-call', callLog.status)} key={callLog.id}>
<div className='call-log-call-header'>
<span className={`codicon codicon-chevron-${isExpanded ? 'down' : 'right'}`} style={{ cursor: 'pointer' }}onClick={() => {
<span className={clsx('codicon', `codicon-chevron-${isExpanded ? 'down' : 'right'}`)} style={{ cursor: 'pointer' }}onClick={() => {
const newOverrides = new Map(expandOverrides);
newOverrides.set(callLog.id, !isExpanded);
setExpandOverrides(newOverrides);
@ -64,7 +64,7 @@ export const CallLogView: React.FC<CallLogProps> = ({
{ callLog.params.url ? <span className='call-log-details'><span className='call-log-url' title={callLog.params.url}>{callLog.params.url}</span></span> : undefined }
{ locator ? <span className='call-log-details'><span className='call-log-selector' title={`page.${locator}`}>{`page.${locator}`}</span></span> : undefined }
{ titleSuffix }
<span className={'codicon ' + iconClass(callLog)}></span>
<span className={clsx('codicon', iconClass(callLog))}></span>
{ typeof callLog.duration === 'number' ? <span className='call-log-time'> {msToString(callLog.duration)}</span> : undefined}
</div>
{ (isExpanded ? callLog.messages : []).map((message, i) => {

View File

@ -17,7 +17,6 @@
import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';

View File

@ -17,7 +17,6 @@
import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { WorkbenchLoader } from './ui/workbenchLoader';

View File

@ -16,7 +16,7 @@
import type { SerializedValue } from '@protocol/channels';
import type { ActionTraceEvent } from '@trace/trace';
import { msToString } from '@web/uiUtils';
import { clsx, msToString } from '@web/uiUtils';
import * as React from 'react';
import './callTab.css';
import { CopyToClipboard } from './copyToClipboard';
@ -71,7 +71,7 @@ function renderProperty(property: Property, key: string) {
text = `"${text}"`;
return (
<div key={key} className='call-line'>
{property.name}:<span className={`call-value ${property.type}`} title={property.text}>{text}</span>
{property.name}:<span className={clsx('call-value', property.type)} title={property.text}>{text}</span>
{ ['string', 'number', 'object', 'locator'].includes(property.type) &&
<CopyToClipboard value={property.text} />
}

View File

@ -20,7 +20,7 @@ import './consoleTab.css';
import type * as modelUtil from './modelUtil';
import { ListView } from '@web/components/listView';
import type { Boundaries } from '../geometry';
import { msToString } from '@web/uiUtils';
import { clsx, msToString } from '@web/uiUtils';
import { ansi2html } from '@web/ansi2html';
import { PlaceholderPanel } from './placeholderPanel';
@ -124,8 +124,8 @@ export const ConsoleTab: React.FunctionComponent<{
render={entry => {
const timestamp = msToString(entry.timestamp - boundaries.minimum);
const timestampElement = <span className='console-time'>{timestamp}</span>;
const errorSuffix = entry.isError ? ' status-error' : entry.isWarning ? ' status-warning' : ' status-none';
const statusElement = entry.browserMessage || entry.browserError ? <span className={'codicon codicon-browser' + errorSuffix} title='Browser message'></span> : <span className={'codicon codicon-file' + errorSuffix} title='Runner message'></span>;
const errorSuffix = entry.isError ? 'status-error' : entry.isWarning ? 'status-warning' : 'status-none';
const statusElement = entry.browserMessage || entry.browserError ? <span className={clsx('codicon', 'codicon-browser', errorSuffix)} title='Browser message'></span> : <span className={clsx('codicon', 'codicon-file', errorSuffix)} title='Runner message'></span>;
let locationText: string | undefined;
let messageBody: JSX.Element[] | string | undefined;
let messageInnerHTML: string | undefined;

View File

@ -20,7 +20,7 @@ import type { ActionTraceEvent } from '@trace/trace';
import { context, prevInList } from './modelUtil';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useMeasure } from '@web/uiUtils';
import { clsx, useMeasure } from '@web/uiUtils';
import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder/recorder';
import ConsoleAPI from '@injected/consoleApi';
@ -209,8 +209,8 @@ export const SnapshotTab: React.FunctionComponent<{
}}>
<BrowserFrame url={snapshotInfo.url} />
<div className='snapshot-switcher'>
<iframe ref={iframeRef0} name='snapshot' title='DOM Snapshot' className={loadingRef.current.visibleIframe === 0 ? 'snapshot-visible' : ''}></iframe>
<iframe ref={iframeRef1} name='snapshot' title='DOM Snapshot' className={loadingRef.current.visibleIframe === 1 ? 'snapshot-visible' : ''}></iframe>
<iframe ref={iframeRef0} name='snapshot' title='DOM Snapshot' className={clsx(loadingRef.current.visibleIframe === 0 && 'snapshot-visible')}></iframe>
<iframe ref={iframeRef1} name='snapshot' title='DOM Snapshot' className={clsx(loadingRef.current.visibleIframe === 1 && 'snapshot-visible')}></iframe>
</div>
</div>
</div>

View File

@ -14,11 +14,12 @@
* limitations under the License.
*/
import { clsx } from '@web/uiUtils';
import './tag.css';
export const TagView: React.FC<{ tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }> = ({ tag, style, onClick }) => {
return <span
className={`tag tag-color-${tagNameToColor(tag)}`}
className={clsx('tag', `tag-color-${tagNameToColor(tag)}`)}
onClick={onClick}
style={{ margin: '6px 0 0 6px', ...style }}
title={`Click to filter by tag: ${tag}`}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { msToString, useMeasure } from '@web/uiUtils';
import { clsx, msToString, useMeasure } from '@web/uiUtils';
import { GlassPane } from '@web/shared/glassPane';
import * as React from 'react';
import type { Boundaries } from '../geometry';
@ -252,11 +252,12 @@ export const Timeline: React.FunctionComponent<{
<div className='timeline-bars'>{
bars.map((bar, index) => {
return <div key={index}
className={'timeline-bar' + (bar.action ? ' action' : '')
+ (bar.resource ? ' network' : '')
+ (bar.consoleMessage ? ' console-message' : '')
+ (bar.active ? ' active' : '')
+ (bar.error ? ' error' : '')}
className={clsx('timeline-bar',
bar.action && 'action',
bar.resource && 'network',
bar.consoleMessage && 'console-message',
bar.active && 'active',
bar.error && 'error')}
style={{
left: bar.leftPosition,
width: Math.max(5, bar.rightPosition - bar.leftPosition),

View File

@ -30,7 +30,7 @@ import { Toolbar } from '@web/components/toolbar';
import type { XtermDataSource } from '@web/components/xtermWrapper';
import { XtermWrapper } from '@web/components/xtermWrapper';
import { useDarkModeSetting } from '@web/theme';
import { settings, useSetting } from '@web/uiUtils';
import { clsx, settings, useSetting } from '@web/uiUtils';
import { statusEx, TestTree } from '@testIsomorphic/testTree';
import type { TreeItem } from '@testIsomorphic/testTree';
import { TestServerConnection } from '@testIsomorphic/testServerConnection';
@ -435,7 +435,7 @@ export const UIModeView: React.FC<{}> = ({
</div>}
<SplitView sidebarSize={250} minSidebarSize={150} orientation='horizontal' sidebarIsFirst={true} settingName='testListSidebar'>
<div className='vbox'>
<div className={'vbox' + (isShowingOutput ? '' : ' hidden')}>
<div className={clsx('vbox', !isShowingOutput && 'hidden')}>
<Toolbar>
<div className='section-title' style={{ flex: 'none' }}>Output</div>
<ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>
@ -444,7 +444,7 @@ export const UIModeView: React.FC<{}> = ({
</Toolbar>
<XtermWrapper source={xtermDataSource}></XtermWrapper>
</div>
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
<TraceView
item={selectedItem}
rootDir={testModel?.config?.rootDir}

View File

@ -36,7 +36,7 @@ import { AttachmentsTab } from './attachmentsTab';
import type { Boundaries } from '../geometry';
import { InspectorTab } from './inspectorTab';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useSetting, msToString } from '@web/uiUtils';
import { useSetting, msToString, clsx } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
@ -251,7 +251,7 @@ export const Workbench: React.FunctionComponent<{
title: 'Actions',
component: <div className='vbox'>
{status && <div className='workbench-run-status'>
<span className={`codicon ${testStatusIcon(status)}`}></span>
<span className={clsx('codicon', testStatusIcon(status))}></span>
<div>{testStatusText(status)}</div>
<div className='spacer'></div>
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import { expect, test } from '@playwright/experimental-ct-react';
import { CodeMirrorWrapper } from './codeMirrorWrapper';

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import { expect, test } from '@playwright/experimental-ct-react';
import { Expandable } from './expandable';

View File

@ -16,6 +16,7 @@
import * as React from 'react';
import './expandable.css';
import { clsx } from '../uiUtils';
export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
title: JSX.Element | string,
@ -23,10 +24,10 @@ export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
expanded: boolean,
expandOnTitleClick?: boolean,
}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => {
return <div className={'expandable' + (expanded ? ' expanded' : '')}>
return <div className={clsx('expandable', expanded && 'expanded')}>
<div className='expandable-title' onClick={() => expandOnTitleClick && setExpanded(!expanded)}>
<div
className={'codicon codicon-' + (expanded ? 'chevron-down' : 'chevron-right')}
className={clsx('codicon', expanded ? 'codicon-chevron-down' : 'codicon-chevron-right')}
style={{ cursor: 'pointer', color: 'var(--vscode-foreground)', marginLeft: '5px' }}
onClick={() => !expandOnTitleClick && setExpanded(!expanded)} />
{title}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import { expect, test } from '@playwright/experimental-ct-react';
import { SplitView } from './splitView';

View File

@ -14,7 +14,7 @@
limitations under the License.
*/
import { useMeasure, useSetting } from '../uiUtils';
import { clsx, useMeasure, useSetting } from '../uiUtils';
import './splitView.css';
import * as React from 'react';
@ -75,7 +75,7 @@ export const SplitView: React.FC<React.PropsWithChildren<SplitViewProps>> = ({
resizerStyle = { right: resizing ? 0 : size - 4, left: resizing ? 0 : undefined, width: resizing ? 'initial' : 8 };
}
return <div className={'split-view ' + orientation + (sidebarIsFirst ? ' sidebar-first' : '') } ref={ref}>
return <div className={clsx('split-view', orientation, sidebarIsFirst && 'sidebar-first') } ref={ref}>
<div className='split-view-main'>{childrenArray[0]}</div>
{ !sidebarHidden && <div style={{ flexBasis: size }} className='split-view-sidebar'>{childrenArray[1]}</div> }
{ !sidebarHidden && <div

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { clsx } from '@web/uiUtils';
import './tabbedPane.css';
import { Toolbar } from './toolbar';
import * as React from 'react';
@ -99,7 +100,7 @@ export const TabbedPaneTab: React.FunctionComponent<{
selected?: boolean,
onSelect: (id: string) => void
}> = ({ id, title, count, errorCount, selected, onSelect }) => {
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
return <div className={clsx('tabbed-pane-tab', selected && 'selected')}
onClick={() => onSelect(id)}
title={title}
key={id}>

View File

@ -14,6 +14,7 @@
limitations under the License.
*/
import { clsx } from '@web/uiUtils';
import './toolbar.css';
import * as React from 'react';
@ -31,5 +32,5 @@ export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
className,
onClick,
}) => {
return <div className={'toolbar' + (noShadow ? ' no-shadow' : '') + (noMinHeight ? ' no-min-height' : '') + ' ' + (className || '')} onClick={onClick}>{children}</div>;
return <div className={clsx('toolbar', noShadow && 'no-shadow', noMinHeight && 'no-min-height', className)} onClick={onClick}>{children}</div>;
};

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';
import type { ImageDiff } from './imageDiffView';
import { ImageDiffView } from './imageDiffView';

View File

@ -191,3 +191,8 @@ export class Settings {
}
export const settings = new Settings();
// inspired by https://www.npmjs.com/package/clsx
export function clsx(...classes: (string | undefined | false)[]) {
return classes.filter(Boolean).join(' ');
}