fix(ui mode): preserve manually selected action in live trace (#22131)

This commit is contained in:
Dmitry Gozman 2023-03-31 18:34:51 -07:00 committed by GitHub
parent 3b9e62432d
commit 82e52004c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 11 deletions

View File

@ -120,6 +120,10 @@ function dedupeAndSortActions(actions: ActionTraceEvent[]) {
return result;
}
export function idForAction(action: ActionTraceEvent) {
return `${action.pageId || 'none'}:${action.callId}`;
}
export function context(action: ActionTraceEvent): ContextEntry {
return (action as any)[contextSymbol];
}

View File

@ -24,7 +24,7 @@ import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic
import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
import type { FullConfig, Suite, TestCase, Location, TestError } from '../../../playwright-test/types/testReporter';
import { SplitView } from '@web/components/splitView';
import { MultiTraceModel } from './modelUtil';
import { idForAction, MultiTraceModel } from './modelUtil';
import './watchMode.css';
import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar';
@ -35,6 +35,7 @@ import { Expandable } from '@web/components/expandable';
import { toggleTheme } from '@web/theme';
import { artifactsFolderName } from '@testIsomorphic/folders';
import { msToString, settings, useSetting } from '@web/uiUtils';
import type { ActionTraceEvent } from '@trace/trace';
let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress | undefined) => void = () => {};
let runWatchedTests = (fileNames: string[]) => {};
@ -468,6 +469,12 @@ const TraceView: React.FC<{
return { outputDir, result };
}, [item]);
// Preserve user selection upon live-reloading trace model by persisting the action id.
// This avoids auto-selection of the last action every time we reload the model.
const [selectedActionId, setSelectedActionId] = React.useState<string | undefined>();
const onSelectionChanged = React.useCallback((action: ActionTraceEvent) => setSelectedActionId(idForAction(action)), [setSelectedActionId]);
const initialSelection = selectedActionId ? model?.actions.find(a => idForAction(a) === selectedActionId) : undefined;
React.useEffect(() => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
@ -514,6 +521,8 @@ const TraceView: React.FC<{
hideStackFrames={true}
showSourcesFirst={true}
rootDir={rootDir}
initialSelection={initialSelection}
onSelectionChanged={onSelectionChanged}
defaultSourceLocation={item.location} />;
};

View File

@ -39,7 +39,9 @@ export const Workbench: React.FunctionComponent<{
showSourcesFirst?: boolean,
rootDir?: string,
defaultSourceLocation?: Location,
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, defaultSourceLocation }) => {
initialSelection?: ActionTraceEvent,
onSelectionChanged?: (action: ActionTraceEvent) => void,
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, defaultSourceLocation, initialSelection, onSelectionChanged }) => {
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined);
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
@ -52,11 +54,18 @@ export const Workbench: React.FunctionComponent<{
if (selectedAction && model?.actions.includes(selectedAction))
return;
const failedAction = model?.actions.find(a => a.error);
if (failedAction)
if (initialSelection && model?.actions.includes(initialSelection))
setSelectedAction(initialSelection);
else if (failedAction)
setSelectedAction(failedAction);
else if (model?.actions.length)
setSelectedAction(model.actions[model.actions.length - 1]);
}, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab]);
}, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab, initialSelection]);
const onActionSelected = React.useCallback((action: ActionTraceEvent) => {
setSelectedAction(action);
onSelectionChanged?.(action);
}, [setSelectedAction, onSelectionChanged]);
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
const consoleCount = errors + warnings;
@ -107,7 +116,7 @@ export const Workbench: React.FunctionComponent<{
<Timeline
model={model}
selectedAction={activeAction}
onSelected={action => setSelectedAction(action)}
onSelected={onActionSelected}
hideTimelineBars={hideTimelineBars}
/>
<SplitView sidebarSize={250} orientation='vertical'>
@ -123,12 +132,8 @@ export const Workbench: React.FunctionComponent<{
sdkLanguage={sdkLanguage}
actions={model?.actions || []}
selectedAction={model ? selectedAction : undefined}
onSelected={action => {
setSelectedAction(action);
}}
onHighlighted={action => {
setHighlightedAction(action);
}}
onSelected={onActionSelected}
onHighlighted={setHighlightedAction}
revealConsole={() => setSelectedPropertiesTab('console')}
/>
},

View File

@ -109,3 +109,104 @@ test('should update trace live', async ({ runUITest, server }) => {
/page.gotohttp:\/\/localhost:\d+\/two.html[\d.]+m?s/
]);
});
test('should preserve action list selection upon live trace update', async ({ runUITest, server, createLatch }) => {
const latch = createLatch();
const { page } = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('live test', async ({ page }) => {
await page.goto('about:blank');
await page.setContent('hello');
${latch.blockingCode}
await page.setContent('world');
await new Promise(() => {});
});
`,
});
// Start test.
await page.getByText('live test').dblclick();
// It should wait on the latch.
const listItem = page.getByTestId('action-list').getByRole('listitem');
await expect(
listItem,
'action list'
).toHaveText([
/browserContext.newPage[\d.]+m?s/,
/page.gotoabout:blank[\d.]+m?s/,
/page.setContent[\d.]+m?s/,
]);
// Manually select page.goto.
await page.getByTestId('action-list').getByText('page.goto').click();
// Generate more actions and check that we are still on the page.goto action.
latch.open();
await expect(
listItem,
'action list'
).toHaveText([
/browserContext.newPage[\d.]+m?s/,
/page.gotoabout:blank[\d.]+m?s/,
/page.setContent[\d.]+m?s/,
/page.setContent[\d.]+m?s/,
]);
await expect(
listItem.locator(':scope.selected'),
'selected action stays the same'
).toHaveText(/page.goto/);
});
test('should update tracing network live', async ({ runUITest, server }) => {
server.setRoute('/style.css', async (req, res) => {
res.end('body { background: red; }');
});
server.setRoute('/one.html', async (req, res) => {
res.end(`
<head>
<link rel=stylesheet href="./style.css"></link>
</head>
<body>
One
</body>
`);
});
const { page } = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('live test', async ({ page }) => {
await page.goto('${server.PREFIX}/one.html');
await page.setContent('hello');
await new Promise(() => {});
});
`,
});
// Start test.
await page.getByText('live test').dblclick();
// It should wait on the latch.
const listItem = page.getByTestId('action-list').getByRole('listitem');
await expect(
listItem,
'action list'
).toHaveText([
/browserContext.newPage[\d.]+m?s/,
/page.gotohttp:\/\/localhost:\d+\/one.html[\d.]+m?s/,
/page.setContent[\d.]+m?s/,
]);
// Once page.setContent is visible, we can be sure that page.goto has all required
// resources in the trace. Switch to it and check that everything renders.
await page.getByTestId('action-list').getByText('page.goto').click();
await expect(
page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('body'),
'verify background'
).toHaveCSS('background-color', 'rgb(255, 0, 0)', { timeout: 15000 });
});