diff --git a/packages/trace-viewer/src/ui/tag.css b/packages/trace-viewer/src/ui/tag.css new file mode 100644 index 0000000000..2cb330e610 --- /dev/null +++ b/packages/trace-viewer/src/ui/tag.css @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tag { + display: inline-block; + padding: 0 8px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + border: 1px solid transparent; + border-radius: 2em; + background-color: #8c959f; + color: white; + margin: 0 10px; + flex: none; + font-weight: 600; +} + +.light-mode .tag-color-0 { + background-color: #ddf4ff; + color: #0550ae; + border: 1px solid #218bff; +} +.light-mode .tag-color-1 { + background-color: #fff8c5; + color: #7d4e00; + border: 1px solid #bf8700; +} +.light-mode .tag-color-2 { + background-color: #fbefff; + color: #6e40c9; + border: 1px solid #a475f9; +} +.light-mode .tag-color-3 { + background-color: #ffeff7; + color: #99286e; + border: 1px solid #e85aad; +} +.light-mode .tag-color-4 { + background-color: #FFF0EB; + color: #9E2F1C; + border: 1px solid #EA6045; +} +.light-mode .tag-color-5 { + background-color: #fff1e5; + color: #9b4215; + border: 1px solid #e16f24; +} + +.dark-mode .tag-color-0 { + background-color: #051d4d; + color: #80ccff; + border: 1px solid #218bff; +} +.dark-mode .tag-color-1 { + background-color: #3b2300; + color: #eac54f; + border: 1px solid #bf8700; +} +.dark-mode .tag-color-2 { + background-color: #271052; + color: #d2a8ff; + border: 1px solid #a475f9; +} +.dark-mode .tag-color-3 { + background-color: #42062a; + color: #ff9bce; + border: 1px solid #e85aad; +} +.dark-mode .tag-color-4 { + background-color: #460701; + color: #FFA28B; + border: 1px solid #EC6547; +} +.dark-mode .tag-color-5 { + background-color: #471700; + color: #ffa657; + border: 1px solid #e16f24; +} \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/tag.tsx b/packages/trace-viewer/src/ui/tag.tsx new file mode 100644 index 0000000000..29ba1546ba --- /dev/null +++ b/packages/trace-viewer/src/ui/tag.tsx @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './tag.css'; + +export const TagView: React.FC<{ tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }> = ({ tag, style, onClick }) => { + return + {tag} + ; +}; + +// 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); +} diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx index 7ca7f95bdc..70574341d9 100644 --- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx @@ -30,6 +30,7 @@ import { testStatusIcon } from './testUtils'; import type { TestModel } from './uiModeModel'; import './uiModeTestListView.css'; import type { TestServerConnection } from '@testIsomorphic/testServerConnection'; +import { TagView } from './tag'; const TestTreeView = TreeView; @@ -46,7 +47,8 @@ export const TestListView: React.FC<{ isLoading?: boolean, onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void, requestedCollapseAllCount: number, -}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount }) => { + setFilterText: (text: string) => void; +}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText }) => { const [treeState, setTreeState] = React.useState({ expandedItems: new Map() }); const [selectedTreeItemId, setSelectedTreeItemId] = React.useState(); const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount); @@ -132,6 +134,21 @@ export const TestListView: React.FC<{ runTests('bounce-if-busy', testTree.collectTestIds(treeItem)); }; + const handleTagClick = (e: React.MouseEvent, tag: string) => { + e.preventDefault(); + e.stopPropagation(); + if (e.metaKey || e.ctrlKey) { + const parts = filterText.split(' '); + if (parts.includes(tag)) + setFilterText(parts.filter(t => t !== tag).join(' ').trim()); + else + setFilterText((filterText + ' ' + tag).trim()); + } else { + // Replace all existing tags with this tag. + setFilterText((filterText.split(' ').filter(t => !t.startsWith('@')).join(' ') + ' ' + tag).trim()); + } + }; + return { return
-
{treeItem.title}
+
+ {treeItem.title} + {treeItem.kind === 'case' ? treeItem.tags.map(tag => handleTagClick(e, tag)} />) : null} +
{!!treeItem.duration && treeItem.status !== 'skipped' &&
{msToString(treeItem.duration)}
} runTreeItem(treeItem)} disabled={!!runningState}> diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index a294e9b32b..a4d53f11b0 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -440,7 +440,9 @@ export const UIModeView: React.FC<{}> = ({ watchedTreeIds={watchedTreeIds} setWatchedTreeIds={setWatchedTreeIds} isLoading={isLoading} - requestedCollapseAllCount={collapseAllCount} /> + requestedCollapseAllCount={collapseAllCount} + setFilterText={setFilterText} + />
; diff --git a/tests/playwright-test/ui-mode-fixtures.ts b/tests/playwright-test/ui-mode-fixtures.ts index a8c2ede79b..fa1ee5c023 100644 --- a/tests/playwright-test/ui-mode-fixtures.ts +++ b/tests/playwright-test/ui-mode-fixtures.ts @@ -75,7 +75,7 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () = const indent = listItem.querySelectorAll('.list-view-indent').length; const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : ''; const selected = listItem.classList.contains('selected') ? ' <=' : ''; - const title = listItem.querySelector('.ui-mode-list-item-title').textContent; + const title = listItem.querySelector('.ui-mode-list-item-title').childNodes[0].textContent; const timeElement = options.time ? listItem.querySelector('.ui-mode-list-item-time') : undefined; const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : ''; result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected); diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index f35d4dec4a..776e74f31e 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -56,6 +56,22 @@ test('should filter by explicit tags', async ({ runUITest }) => { `); }); +test('should display native tags and filter by them on click', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('p', () => {}); + test('pwt', { tag: '@smoke' }, () => {}); + `, + }); + await page.locator('.ui-mode-list-item-title').getByText('smoke').click(); + await expect(page.getByPlaceholder('Filter')).toHaveValue('@smoke'); + await expect.poll(dumpTestTree(page)).toBe(` + ▼ ◯ a.test.ts + ◯ pwt + `); +}); + test('should filter by status', async ({ runUITest }) => { const { page } = await runUITest(basicTestTree);