feat(html): added labels to test cases and test files (#21468)

This commit is contained in:
Alex Neo 2023-03-23 00:35:58 +03:00 committed by GitHub
parent a2a2b78cc8
commit 6947f47f05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 779 additions and 20 deletions

View file

@ -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<string>();
const status = new Set<string>();
const text: string[] = [];
const labels = new Set<string>();
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;
}

View file

@ -31,12 +31,15 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
projectNames: string[],
}>> = ({ 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 (<>

View file

@ -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);
}

View file

@ -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 <Link href={`#?q=p:${value}`}>
<span className={'label label-color-' + (projectNames.indexOf(projectName) % 6)}>
<span className={'label label-color-' + (projectNames.indexOf(projectName) % 6)} style={{ margin: '6px 0 0 6px' }}>
{projectName}
</span>
</Link>;

View file

@ -67,3 +67,9 @@
margin: 0 !important;
}
}
.test-case-project-labels-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}

View file

@ -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 <div className='test-case-column vbox'>
{test && <div className='test-case-path'>{test.path.join(' ')}</div>}
{test && <div className='test-case-title'>{test?.title}</div>}
{test && <div className='test-case-location'>{test.location.file}:{test.location.line}</div>}
{test && !!test.projectName && <ProjectLink projectNames={projectNames} projectName={test.projectName}></ProjectLink>}
{test && (!!test.projectName || labels) && <div className='test-case-project-labels-row'>
{!!test.projectName && <ProjectLink projectNames={projectNames} projectName={test.projectName}></ProjectLink>}
{labels && <LabelsLinkView labels={labels} />}
</div>}
{annotations.size > 0 && <AutoChip header='Annotations'>
{[...annotations].map(annotation => <TestCaseAnnotationView type={annotation[0]} descriptions={annotation[1]} />)}
</AutoChip>}
@ -84,3 +93,19 @@ function retryLabel(index: number) {
return 'Run';
return `Retry #${index}`;
}
const LabelsLinkView: React.FC<React.PropsWithChildren<{
labels: string[],
}>> = ({ labels }) => {
return labels.length > 0 ? (
<>
{labels.map(tag => (
<a style={{ textDecoration: 'none', color: 'var(--color-fg-default)' }} href={`#?q=@${tag}`} >
<span style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={'label label-color-' + (hashStringToInt(tag))}>
{tag}
</span>
</a>
))}
</>
) : null;
};

View file

@ -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;
}

View file

@ -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<React.PropsWithChildren<{
report: HTMLReport;
@ -31,6 +32,8 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
setFileExpanded: (fileId: string, expanded: boolean) => void;
filter: Filter;
}>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
const labels = React.useCallback((test: TestCaseSummary) => matchTags(test?.title).sort((a, b) => a.localeCompare(b)), []);
return <Chip
expanded={isFileExpanded(file.fileId)}
noInsets={true}
@ -40,14 +43,21 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
</span>}>
{file.tests.filter(t => filter.matches(t)).map(test =>
<div key={`test-${test.testId}`} className={'test-file-test test-file-test-outcome-' + test.outcome}>
<div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>
<span style={{ float: 'right', minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
{report.projectNames.length > 1 && !!test.projectName &&
<span style={{ float: 'right' }}><ProjectLink projectNames={report.projectNames} projectName={test.projectName}></ProjectLink></span>}
{statusIcon(test.outcome)}
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' ')}>
<span className='test-file-title'>{[...test.path, test.title].join(' ')}</span>
</Link>
<div className='hbox' style={{ alignItems: 'flex-start' }}>
<div className="hbox">
<span className="test-file-test-status-icon">
{statusIcon(test.outcome)}
</span>
<span>
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' ')}>
<span className='test-file-title'>{[...test.path, test.title].join(' ')}</span>
</Link>
{report.projectNames.length > 1 && !!test.projectName &&
<ProjectLink projectNames={report.projectNames} projectName={test.projectName} />}
<LabelsClickView labels={labels(test)} />
</span>
</div>
<span style={{ minWidth: '50px', textAlign: 'right' }}>{msToString(test.duration)}</span>
</div>
<div className='test-file-details-row'>
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' ')} className='test-file-path-link'>
@ -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 ? <Link href={generateTraceUrl(firstTraces)} title='View trace' className='test-file-badge'>{trace()}</Link> : undefined;
}
const LabelsClickView: React.FC<React.PropsWithChildren<{
labels: string[],
}>> = ({ 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 => (
<span style={{ margin: '6px 0 0 6px', cursor: 'pointer' }} className={'label label-color-' + (hashStringToInt(tag))} onClick={e => onClickHandle(e, tag)}>
{tag}
</span>
))}
</>
) : null;
};

View file

@ -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);
});
});