diff --git a/docs/src/test-annotations-js.md b/docs/src/test-annotations-js.md index 68bdd79848bd5..8ed468af75538 100644 --- a/docs/src/test-annotations-js.md +++ b/docs/src/test-annotations-js.md @@ -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 }) => { // ... diff --git a/packages/html-reporter/src/filter.ts b/packages/html-reporter/src/filter.ts index 9ec4ea7e59691..effea7b9b1340 100644 --- a/packages/html-reporter/src/filter.ts +++ b/packages/html-reporter/src/filter.ts @@ -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; diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index eecbdac9f5753..26cc4d1b73bda 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -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 {url}; + } catch {} + return url; +} + +function TestCaseAnnotationView({ annotation: { type, description, url } }: { annotation: TestCaseAnnotation }) { return (
{type} {description && : {renderAnnotationDescription(description)}} + {url && : {renderAnnotationLink(url)}}
); } diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index f5e3ec0858874..437909569e5f8 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -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; }; diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index da377b099fc17..fa308b3405d61 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -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 = { diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 2a5bbc94d4272..bb0acf19aa153 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -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, @@ -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, diff --git a/packages/playwright/src/reporters/versions/blobV1.ts b/packages/playwright/src/reporters/versions/blobV1.ts index 5ea9350285fcc..4bc3591f5f451 100644 --- a/packages/playwright/src/reporters/versions/blobV1.ts +++ b/packages/playwright/src/reporters/versions/blobV1.ts @@ -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 = { diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 1e9e1f9c46ee1..fb10a8fe5fa22 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -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.', @@ -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); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index e1299272a82a2..9a5ea6ec38217 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1845,6 +1845,7 @@ export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interru type TestDetailsAnnotation = { type: string; description?: string; + url?: string; }; export type TestDetails = { diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 52073066f0efb..913892747653b 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -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; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 83992c0bad55b..e31921be894ad 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -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' }); @@ -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': ` diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 0367f3259ca29..77ac80e6ec0d4 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -68,6 +68,7 @@ export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interru type TestDetailsAnnotation = { type: string; description?: string; + url?: string; }; export type TestDetails = { diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index 51eab7e370508..a69d27651bde7 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -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;