mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-11 12:33:45 +03:00
fix(ui mode): preserve manually selected action in live trace (#22131)
This commit is contained in:
parent
3b9e62432d
commit
82e52004c9
@ -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];
|
||||
}
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
||||
|
@ -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')}
|
||||
/>
|
||||
},
|
||||
|
@ -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 });
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user