feat(ui mode): introduce Testing Options and Settings (#31841)
Testing Options control tests, while Settings are UI mode settings. <img width="298" alt="Screenshot 2024-07-25 at 10 54 22 AM" src="https://github.com/user-attachments/assets/7b6f5fff-687b-48d1-80b3-d1e6f2a257e8"> These sections are separately expandable, collapsed by default. <img width="294" alt="Screenshot 2024-07-24 at 2 06 25 PM" src="https://github.com/user-attachments/assets/5d35ac8c-9289-46ca-aaa2-ebc5419fa0c4"> References #31520. --------- Signed-off-by: Simon Knott <info@simonknott.de> Co-authored-by: Simon Knott <info@simonknott.de>
This commit is contained in:
parent
a966abfd31
commit
a41cebc1c9
|
|
@ -16,13 +16,22 @@
|
||||||
|
|
||||||
.settings-view {
|
.settings-view {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting label {
|
.settings-view .setting label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 6px 2px;
|
margin: 4px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-view .setting:first-of-type label {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-view .setting:last-of-type label {
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-view .setting input {
|
.settings-view .setting input {
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,11 @@ export const SettingsView: React.FunctionComponent<{
|
||||||
settings: Setting<boolean>[],
|
settings: Setting<boolean>[],
|
||||||
}> = ({ settings }) => {
|
}> = ({ settings }) => {
|
||||||
return <div className='vbox settings-view'>
|
return <div className='vbox settings-view'>
|
||||||
{settings.map(setting => {
|
{settings.map(([value, set, title]) => {
|
||||||
return <div key={setting.name} className='setting'>
|
return <div key={title} className='setting'>
|
||||||
<label>
|
<label>
|
||||||
<input type='checkbox' checked={setting.value} onClick={() => {
|
<input type='checkbox' checked={value} onClick={() => set(!value)}/>
|
||||||
setting.set(!setting.value);
|
{title}
|
||||||
}}/>
|
|
||||||
{setting.title}
|
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,13 @@ import type { ContextEntry } from '../entries';
|
||||||
import type { SourceLocation } from './modelUtil';
|
import type { SourceLocation } from './modelUtil';
|
||||||
import { idForAction, MultiTraceModel } from './modelUtil';
|
import { idForAction, MultiTraceModel } from './modelUtil';
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
|
import { type Setting } from '@web/uiUtils';
|
||||||
|
|
||||||
export const TraceView: React.FC<{
|
export const TraceView: React.FC<{
|
||||||
|
showRouteActionsSetting: Setting<boolean>,
|
||||||
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
|
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
}> = ({ item, rootDir }) => {
|
}> = ({ showRouteActionsSetting, item, rootDir }) => {
|
||||||
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
||||||
const [counter, setCounter] = React.useState(0);
|
const [counter, setCounter] = React.useState(0);
|
||||||
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -87,6 +89,7 @@ export const TraceView: React.FC<{
|
||||||
|
|
||||||
return <Workbench
|
return <Workbench
|
||||||
key='workbench'
|
key='workbench'
|
||||||
|
showRouteActionsSetting={showRouteActionsSetting}
|
||||||
model={model?.model}
|
model={model?.model}
|
||||||
showSourcesFirst={true}
|
showSourcesFirst={true}
|
||||||
rootDir={rootDir}
|
rootDir={rootDir}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-mode-sidebar > .settings-toolbar {
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-mode-sidebar > .settings-view {
|
||||||
|
margin: 0 0 3px 23px;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-mode-sidebar input[type=search] {
|
.ui-mode-sidebar input[type=search] {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
||||||
import { XtermWrapper } from '@web/components/xtermWrapper';
|
import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||||
import { toggleTheme } from '@web/theme';
|
import { useDarkModeSetting } from '@web/theme';
|
||||||
import { settings, useSetting } from '@web/uiUtils';
|
import { settings, useSetting } from '@web/uiUtils';
|
||||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||||
|
|
@ -39,6 +39,7 @@ import type { TestModel } from './uiModeModel';
|
||||||
import { FiltersView } from './uiModeFiltersView';
|
import { FiltersView } from './uiModeFiltersView';
|
||||||
import { TestListView } from './uiModeTestListView';
|
import { TestListView } from './uiModeTestListView';
|
||||||
import { TraceView } from './uiModeTraceView';
|
import { TraceView } from './uiModeTraceView';
|
||||||
|
import { SettingsView } from './settingsView';
|
||||||
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
|
|
@ -93,6 +94,37 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const [isDisconnected, setIsDisconnected] = React.useState(false);
|
const [isDisconnected, setIsDisconnected] = React.useState(false);
|
||||||
const [hasBrowsers, setHasBrowsers] = React.useState(true);
|
const [hasBrowsers, setHasBrowsers] = React.useState(true);
|
||||||
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
||||||
|
const [settingsVisible, setSettingsVisible] = React.useState(false);
|
||||||
|
const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false);
|
||||||
|
|
||||||
|
const [runWorkers, setRunWorkers] = React.useState(queryParams.workers);
|
||||||
|
const singleWorkerSetting = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
runWorkers === '1',
|
||||||
|
(value: boolean) => {
|
||||||
|
// When started with `--workers=1`, the setting allows to undo that.
|
||||||
|
// Otherwise, fallback to the cli `--workers=X` argument.
|
||||||
|
setRunWorkers(value ? '1' : (queryParams.workers === '1' ? undefined : queryParams.workers));
|
||||||
|
},
|
||||||
|
'Single worker',
|
||||||
|
] as const;
|
||||||
|
}, [runWorkers, setRunWorkers]);
|
||||||
|
|
||||||
|
const [runHeaded, setRunHeaded] = React.useState(queryParams.headed);
|
||||||
|
const showBrowserSetting = React.useMemo(() => [runHeaded, setRunHeaded, 'Show browser'] as const, [runHeaded, setRunHeaded]);
|
||||||
|
|
||||||
|
const [runUpdateSnapshots, setRunUpdateSnapshots] = React.useState(queryParams.updateSnapshots);
|
||||||
|
const updateSnapshotsSetting = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
runUpdateSnapshots === 'all',
|
||||||
|
(value: boolean) => setRunUpdateSnapshots(value ? 'all' : 'missing'),
|
||||||
|
'Update snapshots',
|
||||||
|
] as const;
|
||||||
|
}, [runUpdateSnapshots, setRunUpdateSnapshots]);
|
||||||
|
|
||||||
|
const [, , showRouteActionsSetting] = useSetting('show-route-actions', true, 'Show route actions');
|
||||||
|
|
||||||
|
const darkModeSetting = useDarkModeSetting();
|
||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -284,11 +316,11 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
grepInvert: queryParams.grepInvert,
|
grepInvert: queryParams.grepInvert,
|
||||||
testIds: [...testIds],
|
testIds: [...testIds],
|
||||||
projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p),
|
projects: [...projectFilters].filter(([_, v]) => v).map(([p]) => p),
|
||||||
workers: queryParams.workers,
|
workers: runWorkers,
|
||||||
timeout: queryParams.timeout,
|
timeout: queryParams.timeout,
|
||||||
headed: queryParams.headed,
|
headed: runHeaded,
|
||||||
outputDir: queryParams.outputDir,
|
outputDir: queryParams.outputDir,
|
||||||
updateSnapshots: queryParams.updateSnapshots,
|
updateSnapshots: runUpdateSnapshots,
|
||||||
reporters: queryParams.reporters,
|
reporters: queryParams.reporters,
|
||||||
trace: 'on',
|
trace: 'on',
|
||||||
});
|
});
|
||||||
|
|
@ -300,7 +332,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
setTestModel({ ...testModel });
|
setTestModel({ ...testModel });
|
||||||
setRunningState(undefined);
|
setRunningState(undefined);
|
||||||
});
|
});
|
||||||
}, [projectFilters, runningState, testModel, testServerConnection]);
|
}, [projectFilters, runningState, testModel, testServerConnection, runWorkers, runHeaded, runUpdateSnapshots]);
|
||||||
|
|
||||||
// Watch implementation.
|
// Watch implementation.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -403,14 +435,13 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
||||||
<TraceView item={selectedItem} rootDir={testModel?.config?.rootDir} />
|
<TraceView showRouteActionsSetting={showRouteActionsSetting} item={selectedItem} rootDir={testModel?.config?.rootDir} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='vbox ui-mode-sidebar'>
|
<div className='vbox ui-mode-sidebar'>
|
||||||
<Toolbar noShadow={true} noMinHeight={true}>
|
<Toolbar noShadow={true} noMinHeight={true}>
|
||||||
<img src='playwright-logo.svg' alt='Playwright logo' />
|
<img src='playwright-logo.svg' alt='Playwright logo' />
|
||||||
<div className='section-title'>Playwright</div>
|
<div className='section-title'>Playwright</div>
|
||||||
<ToolbarButton icon='color-mode' title='Toggle color mode' onClick={() => toggleTheme()} />
|
|
||||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||||
<ToolbarButton icon='terminal' title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
<ToolbarButton icon='terminal' title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
||||||
{!hasBrowsers && <ToolbarButton icon='lightbulb-autofix' style={{ color: 'var(--vscode-list-warningForeground)' }} title='Playwright browsers are missing' onClick={openInstallDialog} />}
|
{!hasBrowsers && <ToolbarButton icon='lightbulb-autofix' style={{ color: 'var(--vscode-list-warningForeground)' }} title='Playwright browsers are missing' onClick={openInstallDialog} />}
|
||||||
|
|
@ -457,6 +488,31 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
requestedCollapseAllCount={collapseAllCount}
|
requestedCollapseAllCount={collapseAllCount}
|
||||||
setFilterText={setFilterText}
|
setFilterText={setFilterText}
|
||||||
/>
|
/>
|
||||||
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
||||||
|
<span
|
||||||
|
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
||||||
|
style={{ marginLeft: 5 }}
|
||||||
|
title={testingOptionsVisible ? 'Hide Testing Options' : 'Show Testing Options'}
|
||||||
|
/>
|
||||||
|
<div className='section-title'>Testing Options</div>
|
||||||
|
</Toolbar>
|
||||||
|
{testingOptionsVisible && <SettingsView settings={[
|
||||||
|
singleWorkerSetting,
|
||||||
|
showBrowserSetting,
|
||||||
|
updateSnapshotsSetting,
|
||||||
|
]} />}
|
||||||
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
||||||
|
<span
|
||||||
|
className={`codicon codicon-${settingsVisible ? 'chevron-down' : 'chevron-right'}`}
|
||||||
|
style={{ marginLeft: 5 }}
|
||||||
|
title={settingsVisible ? 'Hide Settings' : 'Show Settings'}
|
||||||
|
/>
|
||||||
|
<div className='section-title'>Settings</div>
|
||||||
|
</Toolbar>
|
||||||
|
{settingsVisible && <SettingsView settings={[
|
||||||
|
darkModeSetting,
|
||||||
|
showRouteActionsSetting,
|
||||||
|
]} />}
|
||||||
</div>
|
</div>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import { AttachmentsTab } from './attachmentsTab';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
import { InspectorTab } from './inspectorTab';
|
import { InspectorTab } from './inspectorTab';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { useSetting, msToString } from '@web/uiUtils';
|
import { useSetting, msToString, type Setting } from '@web/uiUtils';
|
||||||
import type { Entry } from '@trace/har';
|
import type { Entry } from '@trace/har';
|
||||||
import './workbench.css';
|
import './workbench.css';
|
||||||
import { testStatusIcon, testStatusText } from './testUtils';
|
import { testStatusIcon, testStatusText } from './testUtils';
|
||||||
|
|
@ -53,8 +53,9 @@ export const Workbench: React.FunctionComponent<{
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
|
showRouteActionsSetting?: Setting<boolean>,
|
||||||
openPage?: (url: string, target?: string) => Window | any,
|
openPage?: (url: string, target?: string) => Window | any,
|
||||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => {
|
}> = ({ showRouteActionsSetting, model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => {
|
||||||
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
||||||
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||||
|
|
@ -67,7 +68,11 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||||
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
||||||
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
||||||
const [showRouteActions, , showRouteActionsSetting] = useSetting('show-route-actions', true, 'Show route actions');
|
const [, , showRouteActionsSettingInternal] = useSetting(showRouteActionsSetting ? undefined : 'show-route-actions', true, 'Show route actions');
|
||||||
|
|
||||||
|
const showSettings = !showRouteActionsSetting;
|
||||||
|
showRouteActionsSetting ||= showRouteActionsSettingInternal;
|
||||||
|
const showRouteActions = showRouteActionsSetting[0];
|
||||||
|
|
||||||
const filteredActions = React.useMemo(() => {
|
const filteredActions = React.useMemo(() => {
|
||||||
return (model?.actions || []).filter(action => showRouteActions || action.class !== 'Route');
|
return (model?.actions || []).filter(action => showRouteActions || action.class !== 'Route');
|
||||||
|
|
@ -229,6 +234,40 @@ export const Workbench: React.FunctionComponent<{
|
||||||
else if (model && model.wallTime)
|
else if (model && model.wallTime)
|
||||||
time = Date.now() - model.wallTime;
|
time = Date.now() - model.wallTime;
|
||||||
|
|
||||||
|
const actionsTab: TabbedPaneTabModel = {
|
||||||
|
id: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
component: <div className='vbox'>
|
||||||
|
{status && <div className='workbench-run-status'>
|
||||||
|
<span className={`codicon ${testStatusIcon(status)}`}></span>
|
||||||
|
<div>{testStatusText(status)}</div>
|
||||||
|
<div className='spacer'></div>
|
||||||
|
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
||||||
|
</div>}
|
||||||
|
<ActionList
|
||||||
|
sdkLanguage={sdkLanguage}
|
||||||
|
actions={filteredActions}
|
||||||
|
selectedAction={model ? selectedAction : undefined}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
setSelectedTime={setSelectedTime}
|
||||||
|
onSelected={onActionSelected}
|
||||||
|
onHighlighted={setHighlightedAction}
|
||||||
|
revealConsole={() => selectPropertiesTab('console')}
|
||||||
|
isLive={isLive}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
const metadataTab: TabbedPaneTabModel = {
|
||||||
|
id: 'metadata',
|
||||||
|
title: 'Metadata',
|
||||||
|
component: <MetadataView model={model}/>
|
||||||
|
};
|
||||||
|
const settingsTab: TabbedPaneTabModel = {
|
||||||
|
id: 'settings',
|
||||||
|
title: 'Settings',
|
||||||
|
component: <SettingsView settings={[showRouteActionsSetting]}/>,
|
||||||
|
};
|
||||||
|
|
||||||
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
return <div className='vbox workbench' {...(inert ? { inert: 'true' } : {})}>
|
||||||
<Timeline
|
<Timeline
|
||||||
model={model}
|
model={model}
|
||||||
|
|
@ -254,41 +293,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
setHighlightedLocator={locatorPicked}
|
setHighlightedLocator={locatorPicked}
|
||||||
openPage={openPage} />
|
openPage={openPage} />
|
||||||
<TabbedPane
|
<TabbedPane
|
||||||
tabs={[
|
tabs={showSettings ? [actionsTab, metadataTab, settingsTab] : [actionsTab, metadataTab]}
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
title: 'Actions',
|
|
||||||
component: <div className='vbox'>
|
|
||||||
{status && <div className='workbench-run-status'>
|
|
||||||
<span className={`codicon ${testStatusIcon(status)}`}></span>
|
|
||||||
<div>{testStatusText(status)}</div>
|
|
||||||
<div className='spacer'></div>
|
|
||||||
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
|
||||||
</div>}
|
|
||||||
<ActionList
|
|
||||||
sdkLanguage={sdkLanguage}
|
|
||||||
actions={filteredActions}
|
|
||||||
selectedAction={model ? selectedAction : undefined}
|
|
||||||
selectedTime={selectedTime}
|
|
||||||
setSelectedTime={setSelectedTime}
|
|
||||||
onSelected={onActionSelected}
|
|
||||||
onHighlighted={setHighlightedAction}
|
|
||||||
revealConsole={() => selectPropertiesTab('console')}
|
|
||||||
isLive={isLive}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'metadata',
|
|
||||||
title: 'Metadata',
|
|
||||||
component: <MetadataView model={model}/>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
title: 'Settings',
|
|
||||||
component: <SettingsView settings={[showRouteActionsSetting]}/>,
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
selectedTab={selectedNavigatorTab}
|
selectedTab={selectedNavigatorTab}
|
||||||
setSelectedTab={setSelectedNavigatorTab}
|
setSelectedTab={setSelectedNavigatorTab}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,16 @@ import * as React from 'react';
|
||||||
type ToolbarProps = {
|
type ToolbarProps = {
|
||||||
noShadow?: boolean;
|
noShadow?: boolean;
|
||||||
noMinHeight?: boolean;
|
noMinHeight?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onClick?: (e: React.MouseEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
|
export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
|
||||||
noShadow,
|
noShadow,
|
||||||
children,
|
children,
|
||||||
noMinHeight
|
noMinHeight,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
return <div className={'toolbar' + (noShadow ? ' no-shadow' : '') + (noMinHeight ? ' no-min-height' : '')}>{children}</div>;
|
return <div className={'toolbar' + (noShadow ? ' no-shadow' : '') + (noMinHeight ? ' no-min-height' : '') + ' ' + (className || '')} onClick={onClick}>{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { settings } from './uiUtils';
|
import React from 'react';
|
||||||
|
import { type Setting, settings } from './uiUtils';
|
||||||
|
|
||||||
export function applyTheme() {
|
export function applyTheme() {
|
||||||
if ((document as any).playwrightThemeInitialized)
|
if ((document as any).playwrightThemeInitialized)
|
||||||
|
|
@ -64,3 +65,13 @@ export function removeThemeListener(listener: (theme: Theme) => void) {
|
||||||
export function currentTheme(): Theme {
|
export function currentTheme(): Theme {
|
||||||
return document.body.classList.contains('dark-mode') ? 'dark-mode' : 'light-mode';
|
return document.body.classList.contains('dark-mode') ? 'dark-mode' : 'light-mode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDarkModeSetting() {
|
||||||
|
const [theme, setTheme] = React.useState(currentTheme() === 'dark-mode');
|
||||||
|
return [theme, (value: boolean) => {
|
||||||
|
const current = currentTheme() === 'dark-mode';
|
||||||
|
if (current !== value)
|
||||||
|
toggleTheme();
|
||||||
|
setTheme(value);
|
||||||
|
}, 'Dark mode'] as Setting<boolean>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,12 +139,7 @@ export function copy(text: string) {
|
||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Setting<T> = {
|
export type Setting<T> = readonly [T, (value: T) => void, string];
|
||||||
value: T;
|
|
||||||
set: (value: T) => void;
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useSetting<S>(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch<React.SetStateAction<S>>, Setting<S>] {
|
export function useSetting<S>(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch<React.SetStateAction<S>>, Setting<S>] {
|
||||||
if (name)
|
if (name)
|
||||||
|
|
@ -155,12 +150,7 @@ export function useSetting<S>(name: string | undefined, defaultValue: S, title?:
|
||||||
settings.setObject(name, value);
|
settings.setObject(name, value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
}, [name, setValue]);
|
}, [name, setValue]);
|
||||||
const setting = {
|
const setting = [value, setValueWrapper, title || name || ''] as Setting<S>;
|
||||||
value,
|
|
||||||
set: setValueWrapper,
|
|
||||||
name: name || '',
|
|
||||||
title: title || name || '',
|
|
||||||
};
|
|
||||||
return [value, setValueWrapper, setting];
|
return [value, setValueWrapper, setting];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ test('should work after theme switch', async ({ runUITest, writeFiles }) => {
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
await expect(page.getByTestId('output')).toContainText(`Hello world 1`);
|
await expect(page.getByTestId('output')).toContainText(`Hello world 1`);
|
||||||
|
|
||||||
await page.getByTitle('Toggle color mode').click();
|
await page.getByText('Settings', { exact: true }).click();
|
||||||
|
await page.getByLabel('Dark mode').click();
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue