diff --git a/packages/html-reporter/src/labels.css b/packages/html-reporter/src/labels.css index ad9bdc3a36369..c9775add0ea69 100644 --- a/packages/html-reporter/src/labels.css +++ b/packages/html-reporter/src/labels.css @@ -30,6 +30,16 @@ cursor: pointer; } +.label-button { + font-family: inherit; + cursor: pointer; +} + +.label-button:focus-visible { + outline: 1px solid var(--color-accent-fg); + outline-offset: -1px; +} + .label-anchor { text-decoration: none; color: var(--color-fg-default); diff --git a/packages/html-reporter/src/labels.tsx b/packages/html-reporter/src/labels.tsx index 044e9ea074d9d..d5102f0a64703 100644 --- a/packages/html-reporter/src/labels.tsx +++ b/packages/html-reporter/src/labels.tsx @@ -28,9 +28,14 @@ export const Label: React.FC<{ onClick?: (e: React.MouseEvent, label: string) => void, colorIndex?: number, }> = ({ label, href, onClick, colorIndex, trimAtSymbolPrefix }) => { - const baseLabel = onClick(e, label) : undefined}> - {trimAtSymbolPrefix && label.startsWith('@') ? label.slice(1) : label} - ; + const className = clsx('label', 'label-color-' + (colorIndex !== undefined ? colorIndex : hashStringToInt(label))); + const text = trimAtSymbolPrefix && label.startsWith('@') ? label.slice(1) : label; + + // When there is an href the anchor below is already keyboard accessible, so only a + // standalone click handler needs a button of its own. + const baseLabel = onClick && !href + ? + : onClick(e, label) : undefined}>{text}; return href ? {baseLabel} diff --git a/packages/trace-viewer/src/ui/tag.css b/packages/trace-viewer/src/ui/tag.css index 41317c441aafe..0726ab6f44717 100644 --- a/packages/trace-viewer/src/ui/tag.css +++ b/packages/trace-viewer/src/ui/tag.css @@ -29,6 +29,16 @@ font-weight: 600; } +.tag-button { + font-family: inherit; + cursor: pointer; +} + +.tag-button:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .tag-color-0 { background-color: #ddf4ff; color: #0550ae; diff --git a/packages/trace-viewer/src/ui/tag.tsx b/packages/trace-viewer/src/ui/tag.tsx index 057d70ac66d03..f55f8c342b8a0 100644 --- a/packages/trace-viewer/src/ui/tag.tsx +++ b/packages/trace-viewer/src/ui/tag.tsx @@ -18,20 +18,27 @@ import { clsx } from '@web/uiUtils'; import './tag.css'; export const TagView = ({ tag, style, onClick }: { tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }) => { - return {tag}; + + return ; }; // hash string to integer in range [0, 6] for color index, to get same color for same tag function tagNameToColor(str: string) { let hash = 0; - for (let i = 0; i < str.length; i++) - hash = str.charCodeAt(i) + ((hash << 8) - hash); - return Math.abs(hash % 6); + for (let i = 0; i < str.length; ++i) + hash = str.charCodeAt(i) + ((hash << 5) - hash); + return Math.abs(hash) % 7; } diff --git a/packages/web/src/components/gridView.css b/packages/web/src/components/gridView.css index 65dcd56962088..55cf382fcd307 100644 --- a/packages/web/src/components/gridView.css +++ b/packages/web/src/components/gridView.css @@ -66,6 +66,20 @@ white-space: nowrap; } +.grid-view-header-cell-button { + all: unset; + display: flex; + align-items: center; + flex: auto; + overflow: hidden; + cursor: pointer; +} + +.grid-view-header-cell-button:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .grid-view-header-cell-title { overflow: hidden; text-overflow: ellipsis; diff --git a/packages/web/src/components/gridView.tsx b/packages/web/src/components/gridView.tsx index 33ef2fe51e5c4..5ab58a31989fa 100644 --- a/packages/web/src/components/gridView.tsx +++ b/packages/web/src/components/gridView.tsx @@ -75,17 +75,24 @@ export function GridView(model: GridViewProps) {
{model.columns.map((column, i) => { + const title = <> + {model.columnTitle(column)} + + + ; return
model.setSorting && toggleSorting(column)} > - {model.columnTitle(column)} - - + {model.setSorting ? : title}
; })}
diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 3bad29611ad22..f920fd48cccaf 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -518,6 +518,21 @@ test('should have network requests', async ({ showTraceViewer }) => { await expect(traceViewer.networkRequests.filter({ hasText: '404GET404text' })).toHaveCSS('background-color', 'rgb(242, 222, 222)'); }); +test('should sort network columns from the keyboard', async ({ showTraceViewer }) => { + const traceViewer = await showTraceViewer(traceFile); + await traceViewer.selectAction('Navigate'); + await traceViewer.showNetworkTab(); + + const nameColumn = traceViewer.page.getByRole('button', { name: 'Name', exact: true }); + await expect(nameColumn).toBeVisible(); + + await nameColumn.focus(); + await expect(nameColumn).toBeFocused(); + + await nameColumn.press('Enter'); + await expect(traceViewer.networkRequests.first()).toContainText('404'); +}); + test('should attribute network requests to service workers', async ({ runAndTrace, page, context, server, browserName }) => { test.skip(browserName !== 'chromium', 'Service worker requests are only reported in Chromium'); const traceViewer = await runAndTrace(async () => { diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index b9575303b806b..33e24a496bcdb 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -2145,6 +2145,40 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.test-file-test .test-file-title')).toHaveText('Error Pages › @GCC-1510 fails'); }); + test('should filter by label from the keyboard', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke passes', async ({}) => { + expect(1).toBe(1); + }); + test('@regression passes', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + + await showReport(); + + const smokeLabel = page.locator('.test-file-test', { has: page.getByText('@smoke passes', { exact: true }) }).getByRole('button', { name: 'smoke' }); + await smokeLabel.focus(); + await expect(smokeLabel).toBeFocused(); + await smokeLabel.press('Enter'); + await expect(page.getByPlaceholder('Search tests')).toHaveValue('@smoke '); + await expect(page.locator('.test-file-test')).toHaveCount(1); + + await page.getByPlaceholder('Search tests').clear(); + const regressionLabel = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).getByRole('button', { name: 'regression' }); + await regressionLabel.focus(); + await expect(regressionLabel).toBeFocused(); + await regressionLabel.press('Space'); + await expect(page.getByPlaceholder('Search tests')).toHaveValue('@regression '); + await expect(page.locator('.test-file-test')).toHaveCount(1); + }); + test('tags with special symbols', async ({ runInlineTest, showReport, page }) => { const result = await runInlineTest({ 'a.test.js': ` diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index f549d150e4b09..7e2edd89831b5 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -72,6 +72,23 @@ test('should display native tags and filter by them on click', async ({ runUITes `); }); +test('should filter by tag from the keyboard', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('p', () => {}); + test('pwt', { tag: '@smoke' }, () => {}); + `, + }); + + const tag = page.locator('.ui-mode-tree-item-title').getByRole('button', { name: 'smoke' }); + await tag.focus(); + await expect(tag).toBeFocused(); + + await tag.press('Enter'); + await expect(page.getByPlaceholder('Filter (e.g. text, @tag)')).toHaveValue('@smoke'); +}); + test('should toggle filters from the keyboard', async ({ runUITest }) => { const { page } = await runUITest(basicTestTree); const summary = page.locator('.filter-summary');