Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/logTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const LogTab: React.FunctionComponent<{

return <LogList
name='log'
ariaLabel='Log entries'
items={entries}
render={entry => <div className='log-list-item'>
<span className='log-list-duration'>{entry.time}</span>
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/metadataView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const MetadataView: React.FunctionComponent<{

const wallTime = model.wallTime !== undefined ? new Date(model.wallTime).toLocaleString(undefined, { timeZoneName: 'short' }) : undefined;

return <div data-testid='metadata-view' className='vbox' style={{ flexShrink: 0 }}>
return <div className='vbox' style={{ flexShrink: 0 }}>
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
{!!wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={wallTime}>{wallTime}</span></div>}
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
Expand Down
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/networkTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const NetworkTab: React.FunctionComponent<{

const grid = <NetworkGridView
name='network'
ariaLabel='Network requests'
items={renderedEntries}
selectedItem={selectedEntry}
onSelected={item => setSelectedEntry(item)}
Expand Down
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/stackTrace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const StackTraceView: React.FunctionComponent<{
const frames = stack || [];
return <StackFrameListView
name='stack-trace'
ariaLabel='Stack trace'
items={frames}
selectedItem={frames[selectedFrame]}
render={frame => {
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/gridView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function GridView<T>(model: GridViewProps<T>) {
<ListView
name={model.name}
items={model.items}
ariaLabel={model.ariaLabel}
id={model.id}
render={(item, index) => {
return <>
Expand Down
5 changes: 4 additions & 1 deletion packages/web/src/components/listView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ListViewProps<T> = {
isError?: (item: T, index: number) => boolean,
isWarning?: (item: T, index: number) => boolean,
isInfo?: (item: T, index: number) => boolean,
ariaLabel?: string,
selectedItem?: T,
onAccepted?: (item: T, index: number) => void,
onSelected?: (item: T, index: number) => void,
Expand Down Expand Up @@ -56,6 +57,7 @@ export function ListView<T>({
noItemsMessage,
dataTestId,
notSelectable,
ariaLabel,
}: ListViewProps<T>) {
const itemListRef = React.useRef<HTMLDivElement>(null);
const [highlightedItem, setHighlightedItem] = React.useState<any>();
Expand All @@ -80,7 +82,7 @@ export function ListView<T>({
itemListRef.current.scrollTop = scrollPositions.get(name) || 0;
}, [name]);

return <div className={clsx(`list-view vbox`, name + '-list-view')} role={items.length > 0 ? 'list' : undefined} data-testid={dataTestId || (name + '-list')}>
return <div className={clsx(`list-view vbox`, name + '-list-view')} role={items.length > 0 ? 'list' : undefined} aria-label={ariaLabel}>
<div
className={clsx('list-view-content', notSelectable && 'not-selectable')}
tabIndex={0}
Expand Down Expand Up @@ -132,6 +134,7 @@ export function ListView<T>({
isError?.(item, index) && 'error',
isWarning?.(item, index) && 'warning',
isInfo?.(item, index) && 'info')}
aria-selected={selectedItem === item}
onClick={() => onSelected?.(item, index)}
onMouseEnter={() => setHighlightedItem(item)}
onMouseLeave={() => setHighlightedItem(undefined)}
Expand Down
52 changes: 28 additions & 24 deletions tests/config/traceViewerFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class TraceViewerPage {
errorMessages: Locator;
consoleLineMessages: Locator;
consoleStacks: Locator;
stackFrames: Locator;
networkRequests: Locator;
metadataTab: Locator;
snapshotContainer: Locator;
Expand All @@ -57,74 +56,79 @@ class TraceViewerPage {
this.actionTitles = page.locator('.action-title');
this.actionsTree = page.getByTestId('actions-tree');
this.callLines = page.locator('.call-tab .call-line');
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
this.consoleLines = page.locator('.console-line');
this.logLines = page.getByRole('list', { name: 'Log entries' }).getByRole('listitem');
this.consoleLines = page.getByRole('tabpanel', { name: 'Console' }).getByRole('listitem');
this.consoleLineMessages = page.locator('.console-line-message');
this.errorMessages = page.locator('.error-message');
this.consoleStacks = page.locator('.console-stack');
this.stackFrames = page.getByTestId('stack-trace-list').locator('.list-view-entry');
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
this.networkRequests = page.getByRole('list', { name: 'Network requests' }).getByRole('listitem');
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
this.metadataTab = page.getByTestId('metadata-view');
this.sourceCodeTab = page.getByTestId('source-code');
this.metadataTab = page.getByRole('tabpanel', { name: 'Metadata' });
this.sourceCodeTab = page.getByRole('tabpanel', { name: 'Source' });

this.settingsDialog = page.getByTestId('settings-toolbar-dialog');
this.darkModeSetting = page.locator('.setting').getByText('Dark mode');
this.displayCanvasContentSetting = page.locator('.setting').getByText('Display canvas content');
}

async actionIconsText(action: string) {
const entry = await this.page.waitForSelector(`.tree-view-entry:has-text("${action}")`);
await entry.waitForSelector('.action-icon-value:visible');
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
stackFrames(options: { selected?: boolean } = {}) {
const entry = this.page.getByRole('list', { name: 'Stack trace' }).getByRole('listitem');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we pass { selected } into getByRole. Perhaps we are missing aria-selected somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-selected is unfortunately not valid on listitems: https://www.w3.org/TR/wai-aria-1.2/#aria-selected

We then throw that its not valid (we add aria-listed on the listitem).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting 😄 I guess it should be aria-current then, but we don't have an option for this in getByRole().

if (options.selected)
return entry.locator(':scope.selected');
return entry;
}

async actionIcons(action: string) {
return await this.page.waitForSelector(`.tree-view-entry:has-text("${action}") .action-icons`);
actionIconsText(action: string) {
const entry = this.actionsTree.getByRole('treeitem', { name: action });
return entry.locator('.action-icon-value').filter({ visible: true });
}

actionIcons(action: string) {
return this.actionsTree.getByRole('treeitem', { name: action }).locator('.action-icons').filter({ visible: true });
}

@step
async expandAction(title: string, ordinal: number = 0) {
await this.actionsTree.locator('.tree-view-entry', { hasText: title }).nth(ordinal).locator('.codicon-chevron-right').click();
async expandAction(title: string) {
await this.actionsTree.getByRole('treeitem', { name: title }).locator('.codicon-chevron-right').click();
}

@step
async selectAction(title: string, ordinal: number = 0) {
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).click();
await this.actionsTree.getByTitle(title).nth(ordinal).click();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not getByRole('treeitem', { name: title }) as in previous method?

Copy link
Contributor Author

@mxschmitt mxschmitt Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that we have a nested 'treeitem' - so if we don't make it narrower, it clicks in the middle of the 3 items vs. on the page.goto treeitem which is what we want. Do you have a better alternative in mind?

Screenshot 2025-06-13 at 12 56 04

}

@step
async hoverAction(title: string, ordinal: number = 0) {
await this.page.locator(`.action-title:has-text("${title}")`).nth(ordinal).hover();
await this.actionsTree.getByRole('treeitem', { name: title }).nth(ordinal).hover();
}

@step
async selectSnapshot(name: string) {
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
await this.page.getByRole('tab', { name }).click();
}

async showErrorsTab() {
await this.page.click('text="Errors"');
await this.page.getByRole('tab', { name: 'Errors' }).click();
}

async showConsoleTab() {
await this.page.click('text="Console"');
await this.page.getByRole('tab', { name: 'Console' }).click();
}

async showSourceTab() {
await this.page.click('text="Source"');
await this.page.getByRole('tab', { name: 'Source' }).click();
}

async showNetworkTab() {
await this.page.click('text="Network"');
await this.page.getByRole('tab', { name: 'Network' }).click();
}

async showMetadataTab() {
await this.page.click('text="Metadata"');
await this.page.getByRole('tab', { name: 'Metadata' }).click();
}

async showSettings() {
await this.page.locator('.settings-gear').click();
await this.page.getByRole('button', { name: 'Settings' }).click();
}

@step
Expand Down
25 changes: 12 additions & 13 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,12 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
await expect(listViews.filter({ hasText: 'Cheers!' })).toHaveClass('list-view-entry');
});

test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
test('should open console errors on click', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
expect(await traceViewer.actionIconsText('Evaluate')).toEqual(['2', '1']);
expect(await traceViewer.page.isHidden('.console-tab')).toBeTruthy();
await (await traceViewer.actionIcons('Evaluate')).click();
expect(await traceViewer.page.waitForSelector('.console-tab')).toBeTruthy();
await expect(traceViewer.actionIconsText('Evaluate')).toHaveText(['2', '1']);
await expect(traceViewer.page.getByRole('tabpanel', { name: 'Console' })).toBeHidden();
await traceViewer.actionIcons('Evaluate').click();
await traceViewer.page.getByRole('tabpanel', { name: 'Console' }).waitFor();
});

test('should show params and return value', async ({ showTraceViewer }) => {
Expand Down Expand Up @@ -346,7 +346,7 @@ test('should have correct stack trace', async ({ showTraceViewer }) => {

await traceViewer.selectAction('Click');
await traceViewer.showSourceTab();
await expect(traceViewer.stackFrames).toContainText([
await expect(traceViewer.stackFrames()).toContainText([
/doClick\s+trace-viewer.spec.ts\s+:\d+/,
/recordTrace\s+trace-viewer.spec.ts\s+:\d+/,
], { useInnerText: true });
Expand Down Expand Up @@ -977,14 +977,13 @@ test('should highlight expect failure', async ({ page, server, runAndTrace }) =>
test('should show action source', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('Click');
const page = traceViewer.page;

await page.click('text=Source');
await expect(page.locator('.source-line-running')).toContainText('await page.getByText(\'Click\').click()');
await expect(page.getByTestId('stack-trace-list').locator('.list-view-entry.selected')).toHaveText(/doClick.*trace-viewer\.spec\.ts:[\d]+/);
await traceViewer.showSourceTab();
await expect(traceViewer.page.locator('.source-line-running')).toContainText('await page.getByText(\'Click\').click()');
await expect(traceViewer.stackFrames({ selected: true })).toHaveText(/doClick.*trace-viewer\.spec\.ts:[\d]+/);

await traceViewer.hoverAction('Wait for navigation');
await expect(page.locator('.source-line-running')).toContainText('page.waitForNavigation()');
await expect(traceViewer.page.locator('.source-line-running')).toContainText('page.waitForNavigation()');
});

test('should follow redirects', async ({ page, runAndTrace, server, asset }) => {
Expand Down Expand Up @@ -1015,7 +1014,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>

test('should include metainfo', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.page.locator('text=Metadata').click();
await traceViewer.page.getByRole('tab', { name: 'Metadata' }).click();
const callLine = traceViewer.metadataTab.locator('.call-line');
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
Expand Down Expand Up @@ -1065,7 +1064,7 @@ test('should open two trace files', async ({ context, page, request, server, sho
/Fetch "\/one-style\.css"/,
]);

await traceViewer.page.locator('text=Metadata').click();
await traceViewer.page.getByRole('tab', { name: 'Metadata' }).click();
const callLine = traceViewer.page.locator('.call-line');
// Should get metadata from the context trace
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
Expand Down
6 changes: 3 additions & 3 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,18 +518,18 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await page.getByRole('link', { name: 'passes' }).click();
await page.click('img');
await page.click('.action-title >> text=EVALUATE');
await page.click('text=Source');
await page.getByRole('tab', { name: 'Source' }).click();

await expect(page.locator('.CodeMirror-line')).toContainText([
/import.*test/,
/page\.evaluate/
]);
await expect(page.locator('.source-line-running')).toContainText('page.evaluate');

await expect(page.getByTestId('stack-trace-list')).toContainText([
await expect(page.getByRole('list', { name: 'Stack trace' }).getByRole('listitem')).toContainText([
/a.test.js:[\d]+/,
]);
await expect(page.getByTestId('stack-trace-list').locator('.list-view-entry.selected')).toContainText('a.test.js');
await expect(page.getByRole('list', { name: 'Stack trace' }).getByRole('listitem').and(page.locator('.selected'))).toContainText('a.test.js');
});

test('should not show stack trace', async ({ runInlineTest, page, showReport }) => {
Expand Down
6 changes: 3 additions & 3 deletions tests/playwright-test/ui-mode-test-network-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test('should filter network requests by resource type', async ({ runUITest, serv
await page.getByText('network tab test').dblclick();
await page.getByText('Network', { exact: true }).click();

const networkItems = page.getByTestId('network-list').getByRole('listitem');
const networkItems = page.getByRole('list', { name: 'Network requests' }).getByRole('listitem');

await page.getByText('JS', { exact: true }).click();
await expect(networkItems).toHaveCount(1);
Expand Down Expand Up @@ -73,7 +73,7 @@ test('should filter network requests by url', async ({ runUITest, server }) => {
await page.getByText('network tab test').dblclick();
await page.getByText('Network', { exact: true }).click();

const networkItems = page.getByTestId('network-list').getByRole('listitem');
const networkItems = page.getByRole('list', { name: 'Network requests' }).getByRole('listitem');

await page.getByPlaceholder('Filter network').fill('script.');
await expect(networkItems).toHaveCount(1);
Expand Down Expand Up @@ -198,5 +198,5 @@ test('should not duplicate network entries from beforeAll', {

await page.getByText('first test').dblclick();
await page.getByText('Network', { exact: true }).click();
await expect(page.getByTestId('network-list').getByText('empty.html')).toHaveCount(1);
await expect(page.getByRole('list', { name: 'Network requests' }).getByText('empty.html')).toHaveCount(1);
});
Loading