Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Html report improvements #30094

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/html-reporter/src/copyToClipboard.tsx
Expand Up @@ -34,5 +34,5 @@ export const CopyToClipboard: React.FunctionComponent<{
});
}, [value]);
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
return <button className='copy-icon' onClick={handleCopy}>{iconElement}</button>;
return <button className='copy-icon' onClick={handleCopy} aria-label="copy to clipboard">{iconElement}</button>;
};
17 changes: 15 additions & 2 deletions packages/html-reporter/src/filter.ts
Expand Up @@ -15,12 +15,12 @@
*/

import type { TestCaseSummary } from './types';

export class Filter {
project: string[] = [];
status: string[] = [];
text: string[] = [];
labels: string[] = [];
annotations: string[] = [];

empty(): boolean {
return this.project.length + this.status.length + this.text.length === 0;
Expand All @@ -32,6 +32,7 @@ export class Filter {
const status = new Set<string>();
const text: string[] = [];
const labels = new Set<string>();
const annotations = new Set<string>();
for (const token of tokens) {
if (token.startsWith('p:')) {
project.add(token.slice(2));
Expand All @@ -45,6 +46,10 @@ export class Filter {
labels.add(token);
continue;
}
if (token.startsWith('a:')) {
annotations.add(token.slice(2));
continue;
}
text.push(token.toLowerCase());
}

Expand All @@ -53,6 +58,7 @@ export class Filter {
filter.project = [...project];
filter.status = [...status];
filter.labels = [...labels];
filter.annotations = [...annotations];
return filter;
}

Expand Down Expand Up @@ -114,6 +120,7 @@ export class Filter {
line: String(test.location.line),
column: String(test.location.column),
labels: test.tags.map(tag => tag.toLowerCase()),
annotations: test.annotations.map(a => a.type.toLowerCase() + '=' + a.description?.toLocaleLowerCase())
};
(test as any).searchValues = searchValues;
}
Expand Down Expand Up @@ -144,7 +151,12 @@ export class Filter {
if (!matches)
return false;
}

if (this.annotations.length) {
Copy link
Member

Choose a reason for hiding this comment

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

This will return all the tests w/o annotations as matching the search criteria.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried and it did not . Only test case with annotations will be returned . In below example ,only two test cases which had some annotation is displayed . Am I missing something here?
Screenshot 2024-03-28 at 7 28 31 PM

Copy link
Member

Choose a reason for hiding this comment

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

Ah, you are right, I missed that this is a filter here. Still need a test for every new feature though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have added the test cases for copy feature . Please review .

const matches = this.annotations.every(annotation =>
searchValues.annotations.some(_annotation => _annotation.includes(annotation)));
if (!matches)
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Please add a test to tests/playwright-test/reporter-html.spec.ts that checks that the annotation filter actually works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have added the test as you suggested . Please review .

}
return true;
}
}
Expand All @@ -157,5 +169,6 @@ type SearchValues = {
line: string;
column: string;
labels: string[];
annotations: string[];
};

5 changes: 3 additions & 2 deletions packages/html-reporter/src/headerView.spec.tsx
Expand Up @@ -27,7 +27,7 @@ test('should render counters', async ({ mount }) => {
flaky: 17,
skipped: 10,
ok: false,
}} filterText='' setFilterText={() => {}}></HeaderView>);
}} filterText='' setFilterText={() => { }}></HeaderView>);
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100');
await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
Expand Down Expand Up @@ -59,5 +59,6 @@ test('should toggle filters', async ({ page, mount }) => {
await expect(page).toHaveURL(/#\?q=s:flaky/);
await component.locator('a', { hasText: 'Skipped' }).click();
await expect(page).toHaveURL(/#\?q=s:skipped/);
expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']);
await component.getByRole('searchbox').fill('a:annotation type=annotation description');
expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped', 'a:annotation type=annotation description']);
});
10 changes: 10 additions & 0 deletions packages/html-reporter/src/testCaseView.css
Expand Up @@ -73,4 +73,14 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
}

.annotation-copy-button{
padding-left: 3px;
display: none;
}

.test-case-annotation:hover .annotation-copy-button{
padding-left: 3px;
display: inline;
}
8 changes: 7 additions & 1 deletion packages/html-reporter/src/testCaseView.spec.tsx
Expand Up @@ -64,7 +64,13 @@ const testCase: TestCase = {

test('should render test case', async ({ mount }) => {
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
const firstAnnotationElement = component.getByText('Annotation text', { exact: false }).first();
await expect(firstAnnotationElement).toBeVisible();
await expect(component.getByRole('button', { name: 'copy to clipboard' }).first()).not.toBeVisible();
await expect(component.getByRole('button', { name: 'copy to clipboard' }).nth(1)).not.toBeVisible();
await firstAnnotationElement.hover();
await expect(component.getByRole('button', { name: 'copy to clipboard' }).first()).toBeVisible();
await expect(component.getByRole('button', { name: 'copy to clipboard' }).nth(1)).not.toBeVisible();
await component.getByText('Annotations').click();
await expect(component.getByText('Annotation text')).not.toBeVisible();
await expect(component.getByText('Outer step')).toBeVisible();
Expand Down
6 changes: 4 additions & 2 deletions packages/html-reporter/src/testCaseView.tsx
Expand Up @@ -25,6 +25,7 @@ import './testCaseView.css';
import { TestResultView } from './testResultView';
import { hashStringToInt } from './labelUtils';
import { msToString } from './uiUtils';
import { CopyToClipboard } from './copyToClipboard';

export const TestCaseView: React.FC<{
projectNames: string[],
Expand Down Expand Up @@ -68,15 +69,16 @@ function renderAnnotationDescription(description: string) {
try {
if (['http:', 'https:'].includes(new URL(description).protocol))
return <a href={description} target='_blank' rel='noopener noreferrer'>{description}</a>;
} catch {}
} catch { }
return description;
}

function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) {
return (
<div className='test-case-annotation'>
<span style={{ fontWeight: 'bold' }}>{type}</span>
<span style={{ fontWeight: 'bold', display: 'inline-block', marginBlock: '3px' }}>{type}</span>
{description && <span>: {renderAnnotationDescription(description)}</span>}
<span className='annotation-copy-button'>{description && <CopyToClipboard value={description} />}</span>
</div>
);
}
Expand Down
37 changes: 30 additions & 7 deletions tests/playwright-test/reporter-html.spec.ts
Expand Up @@ -1416,7 +1416,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await showReport();

const searchInput = page.locator('.subnav-search-input');
const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('Error Pages › @smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' });
const smokeLabelButton = page.locator('.test-file-test', { has: page.getByText('Error Pages › @smoke fails', { exact: true }) }).locator('.label', { hasText: 'smoke' });

await expect(smokeLabelButton).toBeVisible();
await smokeLabelButton.click();
Expand All @@ -1426,7 +1426,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await expect(page.locator('.chip', { hasText: 'b.test.js' })).toHaveCount(1);
await expect(page.locator('.test-file-test .test-file-title')).toHaveText('Error Pages › @smoke fails');

const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('Error Pages › @regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' });
const regressionLabelButton = page.locator('.test-file-test', { has: page.getByText('Error Pages › @regression passes', { exact: true }) }).locator('.label', { hasText: 'regression' });

await expect(regressionLabelButton).not.toBeVisible();

Expand Down Expand Up @@ -1538,15 +1538,15 @@ for (const useIntermediateMergeReport of [false, true] as const) {

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' });
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' });
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();
Expand Down Expand Up @@ -1663,8 +1663,8 @@ for (const useIntermediateMergeReport of [false, true] as const) {
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' });
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();
Expand Down Expand Up @@ -1823,7 +1823,7 @@ for (const useIntermediateMergeReport of [false, true] as const) {
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, }) => {
test('handling of meta or ctrl key', async ({ runInlineTest, showReport, page, }) => {
const result = await runInlineTest({
'a.test.js': `
const { expect, test } = require('@playwright/test');
Expand Down Expand Up @@ -2174,6 +2174,29 @@ for (const useIntermediateMergeReport of [false, true] as const) {
await expect(page.getByText('passes title')).toBeVisible();
});

test('tests should filter by annotation texts', async ({ runInlineTest, showReport, page }) => {
const result = await runInlineTest({
'a.test.js': `
const { test, expect } = require('@playwright/test');
test('annotated test',{ annotation :[{type:'key',description:'value'}]}, async ({}) => {expect(1).toBe(1);});
test('non-annotated test', 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');

await searchInput.fill('a:key=value');
await expect(page.getByText('a.test.js', { exact: true })).toBeVisible();
await expect(page.getByText('non-annotated test')).not.toBeVisible();
await expect(page.getByText('annotated test')).toBeVisible();
});

test('tests should filter by fileName:line/column', async ({ runInlineTest, showReport, page }) => {
const result = await runInlineTest({
'a.test.js': `
Expand Down