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

feat(reporting): Add hyperlink rendering for URL fields in HTML reports #31014

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions docs/src/test-annotations-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ test.describe('report tests', {
annotation: [
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23180' },
{ type: 'performance', description: 'very slow test!' },
{ type: 'performance', url: 'https://github.com/microsoft/playwright/issues/23180' },
],
}, async ({ page }) => {
// ...
Expand Down
5 changes: 4 additions & 1 deletion packages/html-reporter/src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ function cacheSearchValues(test: TestCaseSummary): SearchValues {
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())
annotations: test.annotations.map(a => {
const value = a.description?.toLocaleLowerCase() || a.url?.toLocaleLowerCase() || '';
return a.type.toLowerCase() + '=' + value;
}),
};
(test as any)[searchValuesSymbol] = searchValues;
return searchValues;
Expand Down
11 changes: 10 additions & 1 deletion packages/html-reporter/src/testCaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,20 @@ function renderAnnotationDescription(description: string) {
return description;
}

function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) {
function renderAnnotationLink(url: string) {
try {
if (['http:', 'https:'].includes(new URL(url).protocol))
return <a href={url} target='_blank' rel='noopener noreferrer'>{url}</a>;
} catch {}
return url;
}

function TestCaseAnnotationView({ annotation: { type, description, url } }: { annotation: TestCaseAnnotation }) {
return (
<div className='test-case-annotation'>
<span style={{ fontWeight: 'bold' }}>{type}</span>
{description && <span>: {renderAnnotationDescription(description)}</span>}
{url && <span>: {renderAnnotationLink(url)}</span>}
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export type TestEndPayload = {
errors: TestInfoError[];
hasNonRetriableError: boolean;
expectedStatus: TestStatus;
annotations: { type: string, description?: string }[];
annotations: { type: string, description?: string, url?: string }[];
timeout: number;
};

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/isomorphic/teleReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ export type JsonTestCase = {
retries: number;
tags?: string[];
repeatEachIndex: number;
annotations?: { type: string, description?: string }[];
annotations?: { type: string, description?: string, url?: string }[];
};

export type JsonTestEnd = {
testId: string;
expectedStatus: reporterTypes.TestStatus;
timeout: number;
annotations: { type: string, description?: string }[];
annotations: { type: string, description?: string, url?: string }[];
};

export type JsonTestResultStart = {
Expand Down
10 changes: 8 additions & 2 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@ class HtmlBuilder {
location,
duration,
// Annotations can be pushed directly, with a wrong type.
annotations: test.annotations.map(a => ({ type: a.type, description: a.description ? String(a.description) : a.description })),
annotations: test.annotations.map(a => ({
type: a.type,
description: a.description ? String(a.description) : a.description
})),
tags: test.tags,
outcome: test.outcome(),
path,
Expand All @@ -394,7 +397,10 @@ class HtmlBuilder {
location,
duration,
// Annotations can be pushed directly, with a wrong type.
annotations: test.annotations.map(a => ({ type: a.type, description: a.description ? String(a.description) : a.description })),
annotations: test.annotations.map(a => ({
type: a.type,
description: a.description ? String(a.description) : a.description
})),
tags: test.tags,
outcome: test.outcome(),
path,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/reporters/versions/blobV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export type JsonTestEnd = {
testId: string;
expectedStatus: reporterTypes.TestStatus;
timeout: number;
annotations: { type: string, description?: string }[];
annotations: { type: string, description?: string, url?: string }[];
};

export type JsonTestResultStart = {
Expand Down
10 changes: 5 additions & 5 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export class TestInfoImpl implements TestInfo {
this._tracing = new TestTracing(this, workerParams.artifactsDir);
}

private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string]) {
private _modifier(type: 'skip' | 'fail' | 'fixme' | 'slow', modifierArgs: [arg?: any, description?: string, url?: string]) {
if (typeof modifierArgs[1] === 'function') {
throw new Error([
'It looks like you are calling test.skip() inside the test and pass a callback.',
Expand Down Expand Up @@ -473,19 +473,19 @@ export class TestInfoImpl implements TestInfo {
return path.normalize(path.resolve(this._configInternal.configDir, snapshotPath));
}

skip(...args: [arg?: any, description?: string]) {
skip(...args: [arg?: any, description?: string, url?: string]) {
this._modifier('skip', args);
}

fixme(...args: [arg?: any, description?: string]) {
fixme(...args: [arg?: any, description?: string, url?: string]) {
this._modifier('fixme', args);
}

fail(...args: [arg?: any, description?: string]) {
fail(...args: [arg?: any, description?: string, url?: string]) {
this._modifier('fail', args);
}

slow(...args: [arg?: any, description?: string]) {
slow(...args: [arg?: any, description?: string, url?: string]) {
this._modifier('slow', args);
}

Expand Down
1 change: 1 addition & 0 deletions packages/playwright/types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,7 @@ export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interru
type TestDetailsAnnotation = {
type: string;
description?: string;
url?: string;
};

export type TestDetails = {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/types/testReporter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export interface JSONReportSpec {

export interface JSONReportTest {
timeout: number;
annotations: { type: string, description?: string }[],
annotations: { type: string, description?: string, url?: string }[],
expectedStatus: TestStatus;
projectName: string;
projectId: string;
Expand Down
21 changes: 20 additions & 1 deletion tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ for (const useIntermediateMergeReport of [false] as const) {
'a.test.js': `
import { test, expect } from '@playwright/test';
test('annotated test', async ({ page }) => {
test.info().annotations.push({ type: 'issue', description: 'I am not interested in this test' });
test.info().annotations.push({ type: 'issue', description: 'I am not interested in this test' });
});
`,
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
Expand All @@ -736,6 +736,25 @@ for (const useIntermediateMergeReport of [false] as const) {
await expect(page.locator('.test-case-annotation')).toHaveText('issue: I am not interested in this test');
});

test('should render annotations with url', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = { timeout: 1500 };
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('annotated test', async ({ page }) => {
test.info().annotations.push({ type: 'url', url: 'https://github.com/microsoft/playwright/pull/31014' });
});
`,
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
await showReport();
await page.click('text=annotated test');
await expect(page.locator('.test-case-annotation')).toHaveText(`url: https://github.com/microsoft/playwright/pull/31014`);
});

test('should render annotations as link if needed', async ({ runInlineTest, page, showReport, server }) => {
const result = await runInlineTest({
'playwright.config.js': `
Expand Down
1 change: 1 addition & 0 deletions utils/generate_types/overrides-test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interru
type TestDetailsAnnotation = {
type: string;
description?: string;
url?: string;
};

export type TestDetails = {
Expand Down
2 changes: 1 addition & 1 deletion utils/generate_types/overrides-testReporter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export interface JSONReportSpec {

export interface JSONReportTest {
timeout: number;
annotations: { type: string, description?: string }[],
annotations: { type: string, description?: string, url?: string }[],
expectedStatus: TestStatus;
projectName: string;
projectId: string;
Expand Down
Loading