diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 5718cdc172..3177128f17 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -14,12 +14,14 @@ limitations under the License. */ +import { escapeRegExp } from './labelUtils'; import type { TestCaseSummary } from './types'; export class Filter { project: string[] = []; status: string[] = []; text: string[] = []; + labels: string[] = []; empty(): boolean { return this.project.length + this.status.length + this.text.length === 0; @@ -30,6 +32,7 @@ export class Filter { const project = new Set(); const status = new Set(); const text: string[] = []; + const labels = new Set(); for (const token of tokens) { if (token.startsWith('p:')) { project.add(token.slice(2)); @@ -39,6 +42,10 @@ export class Filter { status.add(token.slice(2)); continue; } + if (token.startsWith('@')) { + labels.add(token); + continue; + } text.push(token.toLowerCase()); } @@ -46,6 +53,7 @@ export class Filter { filter.text = text; filter.project = [...project]; filter.status = [...status]; + filter.labels = [...labels]; return filter; } @@ -102,7 +110,7 @@ export class Filter { const searchValues: SearchValues = { text: (status + ' ' + test.projectName + ' ' + test.path.join(' ') + test.title).toLowerCase(), project: test.projectName.toLowerCase(), - status: status as any + status: status as any, }; (test as any).searchValues = searchValues; } @@ -118,12 +126,16 @@ export class Filter { if (!matches) return false; } - if (this.text.length) { const matches = this.text.filter(t => searchValues.text.includes(t)).length === this.text.length; if (!matches) return false; } + if (this.labels.length) { + const matches = this.labels.every(l => searchValues.text?.match(new RegExp(`(\\s|^)${escapeRegExp(l)}(\\s|$)`, 'g'))); + if (!matches) + return false; + } return true; } diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index 2e2a47ace1..11c9a2d724 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -31,12 +31,15 @@ export const HeaderView: React.FC> = ({ stats, filterText, setFilterText, projectNames }) => { React.useEffect(() => { - (async () => { - window.addEventListener('popstate', () => { - const params = new URLSearchParams(window.location.hash.slice(1)); - setFilterText(params.get('q') || ''); - }); - })(); + const popstateFn = () => { + const params = new URLSearchParams(window.location.hash.slice(1)); + setFilterText(params.get('q') || ''); + }; + window.addEventListener('popstate', popstateFn); + + return () => { + window.removeEventListener('popstate', popstateFn); + }; }, [setFilterText]); return (<> diff --git a/packages/html-reporter/src/labelUtils.tsx b/packages/html-reporter/src/labelUtils.tsx new file mode 100644 index 0000000000..84d1f56ede --- /dev/null +++ b/packages/html-reporter/src/labelUtils.tsx @@ -0,0 +1,37 @@ +/** + * 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. + */ + +export function escapeRegExp(string: string) { + const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + const reHasRegExpChar = RegExp(reRegExpChar.source); + + return (string && reHasRegExpChar.test(string)) + ? string.replace(reRegExpChar, '\\$&') + : (string || ''); +} + +// match all tags in test title +export function matchTags(title: string): string[] { + return title.match(/@(\w+)/g)?.map(tag => tag.slice(1)) || []; +} + +// hash string to integer in range [0, 6] for color index, to get same color for same tag +export function hashStringToInt(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/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index e8cbe282a4..03eae930a6 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -55,7 +55,7 @@ export const ProjectLink: React.FunctionComponent<{ const encoded = encodeURIComponent(projectName); const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`; return - + {projectName} ; diff --git a/packages/html-reporter/src/testCaseView.css b/packages/html-reporter/src/testCaseView.css index 5aa3e006e6..ae2568977e 100644 --- a/packages/html-reporter/src/testCaseView.css +++ b/packages/html-reporter/src/testCaseView.css @@ -67,3 +67,9 @@ margin: 0 !important; } } + +.test-case-project-labels-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} \ No newline at end of file diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index d8e164f979..0e966ed0de 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -23,6 +23,7 @@ import { ProjectLink } from './links'; import { statusIcon } from './statusIcon'; import './testCaseView.css'; import { TestResultView } from './testResultView'; +import { hashStringToInt, matchTags } from './labelUtils'; export const TestCaseView: React.FC<{ projectNames: string[], @@ -38,12 +39,20 @@ export const TestCaseView: React.FC<{ list.push(annotation.description); annotations.set(annotation.type, list); }); + const labels = React.useMemo(() => { + if (!test) + return undefined; + return matchTags(test.title).sort((a, b) => a.localeCompare(b)); + }, [test]); return
{test &&
{test.path.join(' › ')}
} {test &&
{test?.title}
} {test &&
{test.location.file}:{test.location.line}
} - {test && !!test.projectName && } + {test && (!!test.projectName || labels) &&
+ {!!test.projectName && } + {labels && } +
} {annotations.size > 0 && {[...annotations].map(annotation => )} } @@ -84,3 +93,19 @@ function retryLabel(index: number) { return 'Run'; return `Retry #${index}`; } + +const LabelsLinkView: React.FC> = ({ labels }) => { + return labels.length > 0 ? ( + <> + {labels.map(tag => ( + + + {tag} + + + ))} + + ) : null; +}; diff --git a/packages/html-reporter/src/testFileView.css b/packages/html-reporter/src/testFileView.css index b996667563..72858846bb 100644 --- a/packages/html-reporter/src/testFileView.css +++ b/packages/html-reporter/src/testFileView.css @@ -18,7 +18,6 @@ line-height: 32px; align-items: center; padding: 2px 10px; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @@ -67,3 +66,7 @@ .test-file-test-outcome-skipped { color: var(--color-fg-muted); } + +.test-file-test-status-icon { + flex: none; +} \ No newline at end of file diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index c80f6535af..8a5b15658e 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -19,10 +19,11 @@ import * as React from 'react'; import { msToString } from './uiUtils'; import { Chip } from './chip'; import type { Filter } from './filter'; -import { generateTraceUrl, Link, ProjectLink } from './links'; +import { generateTraceUrl, Link, navigate, ProjectLink } from './links'; import { statusIcon } from './statusIcon'; import './testFileView.css'; import { video, image, trace } from './icons'; +import { hashStringToInt, matchTags } from './labelUtils'; export const TestFileView: React.FC void; filter: Filter; }>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => { + const labels = React.useCallback((test: TestCaseSummary) => matchTags(test?.title).sort((a, b) => a.localeCompare(b)), []); + return }> {file.tests.filter(t => filter.matches(t)).map(test =>
-
- {msToString(test.duration)} - {report.projectNames.length > 1 && !!test.projectName && - } - {statusIcon(test.outcome)} - - {[...test.path, test.title].join(' › ')} - +
+
+ + {statusIcon(test.outcome)} + + + + {[...test.path, test.title].join(' › ')} + + {report.projectNames.length > 1 && !!test.projectName && + } + + +
+ {msToString(test.duration)}
@@ -78,3 +88,40 @@ function traceBadge(test: TestCaseSummary): JSX.Element | undefined { const firstTraces = test.results.map(result => result.attachments.filter(attachment => attachment.name === 'trace')).filter(traces => traces.length > 0)[0]; return firstTraces ? {trace()} : undefined; } + +const LabelsClickView: React.FC> = ({ labels }) => { + + const onClickHandle = (e: React.MouseEvent, tag: string) => { + e.preventDefault(); + const searchParams = new URLSearchParams(window.location.hash.slice(1)); + let q = searchParams.get('q')?.toString() || ''; + + // if metaKey or ctrlKey is pressed, add tag to search query without replacing existing tags + // if metaKey or ctrlKey is pressed and tag is already in search query, remove tag from search query + if (e.metaKey || e.ctrlKey) { + if (!q.includes(`@${tag}`)) + q = `${q} @${tag}`.trim(); + else + q = q.split(' ').filter(t => t !== `@${tag}`).join(' ').trim(); + // if metaKey or ctrlKey is not pressed, replace existing tags with new tag + } else { + if (!q.includes('@')) + q = `${q} @${tag}`.trim(); + else + q = (q.split(' ').filter(t => !t.startsWith('@')).join(' ').trim() + ` @${tag}`).trim(); + } + navigate(q ? `#?q=${q}` : '#'); + }; + + return labels.length > 0 ? ( + <> + {labels.map(tag => ( + onClickHandle(e, tag)}> + {tag} + + ))} + + ) : null; +}; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 6628a1a39e..daceb9bc45 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1017,3 +1017,629 @@ test.describe('report location', () => { expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report'))).toBe(true); }); }); + +test.describe('labels', () => { + test('should show labels in the test row', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + retries: 1, + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], + }; + `, + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke @passed passed', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke @failed failed', async ({}) => { + expect(1).toBe(2); + }); + `, + 'c.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression @failed failed', async ({}) => { + expect(1).toBe(2); + }); + test('@regression @flaky flaky', async ({}, testInfo) => { + if (testInfo.retry) + expect(1).toBe(1); + else + expect(1).toBe(2); + }); + test.skip('@regression skipped', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(3); + expect(result.failed).toBe(6); + + await showReport(); + + await expect(page.locator('.test-file-test .label')).toHaveCount(42); + await expect(page.locator('.test-file-test', { has: page.getByText('@regression @failed failed', { exact: true }) }).locator('.label')).toHaveText([ + 'chromium', + 'failed', + 'regression', + 'firefox', + 'failed', + 'regression', + 'webkit', + 'failed', + 'regression' + ]); + await expect(page.locator('.test-file-test', { has: page.getByText('@regression @flaky flaky', { exact: true }) }).locator('.label')).toHaveText([ + 'chromium', + 'flaky', + 'regression', + 'firefox', + 'flaky', + 'regression', + 'webkit', + 'flaky', + 'regression', + ]); + await expect(page.locator('.test-file-test', { has: page.getByText('@regression skipped', { exact: true }) }).locator('.label')).toHaveText([ + 'chromium', + 'regression', + 'firefox', + 'regression', + 'webkit', + 'regression', + ]); + await expect(page.locator('.test-file-test', { has: page.getByText('@smoke @passed passed', { exact: true }) }).locator('.label')).toHaveText([ + 'chromium', + 'passed', + 'smoke', + 'firefox', + 'passed', + 'smoke', + 'webkit', + 'passed', + 'smoke' + ]); + await expect(page.locator('.test-file-test', { has: page.getByText('@smoke @failed failed', { exact: true }) }).locator('.label')).toHaveText([ + 'chromium', + 'failed', + 'smoke', + 'firefox', + 'failed', + 'smoke', + 'webkit', + 'failed', + 'smoke', + ]); + }); + + test('project label still shows up without test labels', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], + }; + `, + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('pass', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + + await showReport(); + + await expect(page.locator('.test-file-test .label')).toHaveCount(3); + await expect(page.locator('.test-file-test', { has: page.getByText('pass', { exact: true }) }).locator('.label')).toHaveText(['chromium', 'firefox', 'webkit']); + await page.locator('.test-file-test', { has: page.getByText('chromium', { exact: true }) }).locator('.test-file-title').click(); + await expect(page).toHaveURL(/testId/); + await expect(page.locator('.label')).toHaveCount(1); + await expect(page.locator('.label')).toHaveText('chromium'); + await page.goBack(); + await page.locator('.test-file-test', { has: page.getByText('firefox', { exact: true }) }).locator('.test-file-title').click(); + await expect(page).toHaveURL(/testId/); + await expect(page.locator('.label')).toHaveCount(1); + await expect(page.locator('.label')).toHaveText('firefox'); + await page.goBack(); + await page.locator('.test-file-test', { has: page.getByText('webkit', { exact: true }) }).locator('.test-file-title').click(); + await expect(page).toHaveURL(/testId/); + await expect(page.locator('.label')).toHaveCount(1); + await expect(page.locator('.label')).toHaveText('webkit'); + }); + + test('testCaseView - after click test label and go back, testCaseView should be visible', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], + }; + `, + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@flaky pass', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + + await expect(page.locator('.test-file-test .label')).toHaveCount(6); + await expect(page.locator('.test-file-test', { has: page.getByText('chromium', { exact: true }) }).locator('.label')).toHaveText(['chromium', 'flaky']); + await page.locator('.test-file-test', { has: page.getByText('chromium', { exact: true }) }).locator('.test-file-title').click(); + await expect(page).toHaveURL(/testId/); + await expect(page.locator('.label')).toHaveCount(2); + await expect(page.locator('.label')).toHaveText(['chromium', 'flaky']); + await page.locator('.label', { has: page.getByText('flaky', { exact: true }) }).click(); + await expect(page).not.toHaveURL(/testId/); + await expect(searchInput).toHaveValue('@flaky'); + await page.goBack(); + await expect(page).toHaveURL(/testId/); + await expect(page.locator('.label')).toHaveCount(2); + await expect(page.locator('.label')).toHaveText(['chromium', 'flaky']); + }); + + test('tests with long title should not ellipsis', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], + }; + `, + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@finally @oddly @questioningly @sleepily @warmly @healthily @smoke @flaky this is a very long test title that should not overflow and should be truncated. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.failed).toBe(0); + + await showReport(); + + const firstTitle = page.locator('.test-file-title', { hasText: '@finally @oddly @questioningly @sleepily @warmly @healthily @smoke @flaky ' }).first(); + await expect(firstTitle).toBeVisible(); + expect((await firstTitle.boundingBox()).height).toBeGreaterThanOrEqual(100); + }); + + test('should show filtered tests by labels when click on label', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke fails', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.failed).toBe(1); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('@smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' }); + + await expect(smokeLabelButton).toBeVisible(); + await smokeLabelButton.click(); + await expect(searchInput).toHaveValue('@smoke'); + await expect(page.locator('.test-file-test')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title')).toHaveText('@smoke fails'); + + const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' }); + + await expect(regressionLabelButton).not.toBeVisible(); + + await searchInput.clear(); + + await expect(regressionLabelButton).toBeVisible(); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + + await regressionLabelButton.click(); + await expect(searchInput).toHaveValue('@regression'); + await expect(page.locator('.test-file-test')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.test-file-test .test-file-title')).toHaveText('@regression passes'); + }); + + test('click label should change URL', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke fails', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.failed).toBe(1); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + + const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('@smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' }); + await smokeLabelButton.click(); + await expect(page).toHaveURL(/@smoke/); + await searchInput.clear(); + await page.keyboard.press('Enter'); + await expect(searchInput).toHaveValue(''); + await expect(page).not.toHaveURL(/@smoke/); + + const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' }); + await regressionLabelButton.click(); + await expect(page).toHaveURL(/@regression/); + await searchInput.clear(); + await page.keyboard.press('Enter'); + await expect(searchInput).toHaveValue(''); + await expect(page).not.toHaveURL(/@regression/); + }); + + test('labels whould be applied together with status filter', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression passes', async ({}) => { + expect(1).toBe(1); + }); + + test('@smoke passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke fails', async ({}) => { + expect(1).toBe(2); + }); + + test('@regression fails', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(2); + expect(result.failed).toBe(2); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + const passedNavMenu = page.locator('.subnav-item:has-text("Passed")'); + const failedNavMenu = page.locator('.subnav-item:has-text("Failed")'); + const allNavMenu = page.locator('.subnav-item:has-text("All")'); + const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('@smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' }); + const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('@regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' }); + + await failedNavMenu.click(); + await smokeLabelButton.click(); + await expect(page.locator('.test-file-test')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title')).toHaveText('@smoke fails'); + await expect(searchInput).toHaveValue('s:failed @smoke'); + await expect(page).toHaveURL(/s:failed%20@smoke/); + + await passedNavMenu.click(); + await regressionLabelButton.click(); + await expect(page.locator('.test-file-test')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.test-file-test .test-file-title')).toHaveText('@regression passes'); + await expect(searchInput).toHaveValue('s:passed @regression'); + await expect(page).toHaveURL(/s:passed%20@regression/); + + await allNavMenu.click(); + await regressionLabelButton.click(); + await expect(page.locator('.test-file-test')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(2); + await expect(searchInput).toHaveValue('@regression'); + await expect(page).toHaveURL(/@regression/); + }); + + test('tests should be filtered by label input in search field', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression passes', async ({}) => { + expect(1).toBe(1); + }); + + test('@smoke passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke fails', async ({}) => { + expect(1).toBe(2); + }); + + test('@regression fails', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(2); + expect(result.failed).toBe(2); + + await showReport(); + + const searchInput = page.locator('.subnav-search-input'); + + await searchInput.fill('@smoke'); + await searchInput.press('Enter'); + await expect(page.locator('.test-file-test')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(2); + await expect(searchInput).toHaveValue('@smoke'); + await expect(page).toHaveURL(/%40smoke/); + + await searchInput.fill('@regression'); + await searchInput.press('Enter'); + await expect(page.locator('.test-file-test')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(2); + await expect(searchInput).toHaveValue('@regression'); + await expect(page).toHaveURL(/%40regression/); + }); + + test('if label contains similar words only one label should be selected', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@company passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@company_information fails', async ({}) => { + expect(1).toBe(2); + }); + `, + 'c.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@company_information_widget fails', async ({}) => { + expect(1).toBe(2); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(1); + expect(result.failed).toBe(2); + + await showReport(); + + await expect(page.locator('.chip')).toHaveCount(3); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await expect(page.locator('.test-file-test')).toHaveCount(3); + await expect(page.locator('.test-file-test .test-file-title')).toHaveCount(3); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company passes' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company_information fails' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company_information_widget fails' })).toHaveCount(1); + + const searchInput = page.locator('.subnav-search-input'); + const companyLabelButton = page.locator('.test-file-test', { has: page.getByText('@company passes') }).locator('.label', { hasText: 'company' }); + const companyInformationLabelButton = page.locator('.test-file-test', { has: page.getByText('@company_information fails') }).locator('.label', { hasText: 'company_information' }); + const companyInformationWidgetLabelButton = page.locator('.test-file-test', { has: page.getByText('@company_information_widget fails') }).locator('.label', { hasText: 'company_information_widget' }); + + await expect(companyLabelButton).toBeVisible(); + await expect(companyInformationLabelButton).toBeVisible(); + await expect(companyInformationWidgetLabelButton).toBeVisible(); + + await companyLabelButton.click(); + await expect(page.locator('.chip')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(0); + + await searchInput.clear(); + + await companyInformationLabelButton.click(); + await expect(page.locator('.chip')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(0); + + await searchInput.clear(); + + await companyInformationWidgetLabelButton.click(); + await expect(page.locator('.chip')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await searchInput.clear(); + + await expect(page.locator('.test-file-test')).toHaveCount(3); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company passes' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company_information fails' })).toHaveCount(1); + await expect(page.locator('.test-file-test .test-file-title', { hasText: '@company_information_widget fails' })).toHaveCount(1); + }); + + test('handling of meta or ctrl key', async ({ runInlineTest, showReport, page, }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke @regression passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'b.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@smoke @flaky passes', async ({}) => { + expect(1).toBe(1); + }); + `, + 'c.test.js': ` + const { expect, test } = require('@playwright/test'); + test('@regression @flaky passes', async ({}) => { + expect(1).toBe(1); + }); + `, + }, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.failed).toBe(0); + + await showReport(); + + const smokeButton = page.locator('.label', { hasText: 'smoke' }).first(); + const regressionButton = page.locator('.label', { hasText: 'regression' }).first(); + const flakyButton = page.locator('.label', { hasText: 'flaky' }).first(); + const searchInput = page.locator('.subnav-search-input'); + + await expect(page.locator('.chip')).toHaveCount(3); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await page.keyboard.down(process.platform === 'darwin' ? 'Meta' : 'Control'); + await smokeButton.click(); + + await expect(searchInput).toHaveValue('@smoke'); + await expect(page).toHaveURL(/@smoke/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(0); + + await regressionButton.click(); + + await expect(searchInput).toHaveValue('@smoke @regression'); + await expect(page).toHaveURL(/@smoke%20@regression/); + await expect(page.locator('.chip')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(0); + + await smokeButton.click(); + + await expect(searchInput).toHaveValue('@regression'); + await expect(page).toHaveURL(/@regression/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await flakyButton.click(); + + await expect(searchInput).toHaveValue('@regression @flaky'); + await expect(page).toHaveURL(/@regression%20@flaky/); + await expect(page.locator('.chip')).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await regressionButton.click(); + + await expect(searchInput).toHaveValue('@flaky'); + await expect(page).toHaveURL(/@flaky/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await flakyButton.click(); + + await expect(searchInput).toHaveValue(''); + await expect(page).not.toHaveURL(/@/); + await expect(page.locator('.chip')).toHaveCount(3); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await page.keyboard.up(process.platform === 'darwin' ? 'Meta' : 'Control'); + await smokeButton.click(); + + await expect(searchInput).toHaveValue('@smoke'); + await expect(page).toHaveURL(/@smoke/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(0); + + await regressionButton.click(); + + await expect(searchInput).toHaveValue('@regression'); + await expect(page).toHaveURL(/@regression/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + + await flakyButton.click(); + + await expect(searchInput).toHaveValue('@flaky'); + await expect(page).toHaveURL(/@flaky/); + await expect(page.locator('.chip')).toHaveCount(2); + await expect(page.locator('.chip', { hasText: 'a.test.js' })).toHaveCount(0); + await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1); + await expect(page.locator('.chip', { hasText: 'c.test.js' })).toHaveCount(1); + }); +}); \ No newline at end of file