diff --git a/packages/html-reporter/src/labelUtils.tsx b/packages/html-reporter/src/labelUtils.tsx
deleted file mode 100644
index 014ec77d59..0000000000
--- a/packages/html-reporter/src/labelUtils.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * 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.
- */
-
-// 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 cededa0dbc..4ddaf20a91 100644
--- a/packages/html-reporter/src/links.tsx
+++ b/packages/html-reporter/src/links.tsx
@@ -20,6 +20,7 @@ import * as icons from './icons';
import { TreeItem } from './treeItem';
import { CopyToClipboard } from './copyToClipboard';
import './links.css';
+import { linkifyText } from './renderUtils';
export function navigate(href: string) {
window.history.pushState({}, '', href);
@@ -77,9 +78,9 @@ export const AttachmentLink: React.FunctionComponent<{
return
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
{attachment.path && {linkName || attachment.name}}
- {attachment.body && {attachment.name}}
+ {attachment.body && {linkifyText(attachment.name)}}
} loadChildren={attachment.body ? () => {
- return [{attachment.body}
];
+ return [{linkifyText(attachment.body!)}
];
} : undefined} depth={0} style={{ lineHeight: '32px' }}>;
};
diff --git a/packages/html-reporter/src/renderUtils.tsx b/packages/html-reporter/src/renderUtils.tsx
new file mode 100644
index 0000000000..e8d92dc609
--- /dev/null
+++ b/packages/html-reporter/src/renderUtils.tsx
@@ -0,0 +1,47 @@
+/**
+ * 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 linkifyText(description: string) {
+ const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f';
+ const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug');
+
+ const result = [];
+ let currentIndex = 0;
+ let match;
+
+ while ((match = WEB_LINK_REGEX.exec(description)) !== null) {
+ const stringBeforeMatch = description.substring(currentIndex, match.index);
+ if (stringBeforeMatch)
+ result.push(stringBeforeMatch);
+
+ const value = match[0];
+ result.push(renderLink(value));
+ currentIndex = match.index + value.length;
+ }
+ const stringAfterMatches = description.substring(currentIndex);
+ if (stringAfterMatches)
+ result.push(stringAfterMatches);
+
+ return result;
+}
+
+function renderLink(text: string) {
+ let link = text;
+ if (link.startsWith('www.'))
+ link = 'https://' + link;
+
+ return {text};
+}
diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx
index 3bde6e8a83..d73407582d 100644
--- a/packages/html-reporter/src/testCaseView.spec.tsx
+++ b/packages/html-reporter/src/testCaseView.spec.tsx
@@ -77,7 +77,7 @@ test('should render test case', async ({ mount }) => {
await expect(component.getByText('My test')).toBeVisible();
});
-const linkRenderingTestCase: TestCase = {
+const annotationLinkRenderingTestCase: TestCase = {
testId: 'testid',
title: 'My test',
path: [],
@@ -96,8 +96,7 @@ const linkRenderingTestCase: TestCase = {
};
test('should correctly render links in annotations', async ({ mount }) => {
- const component = await mount();
- // const container = await(component.getByText('Annotations'));
+ const component = await mount();
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
await expect(firstLink).toBeVisible();
@@ -114,4 +113,48 @@ test('should correctly render links in annotations', async ({ mount }) => {
const fourthLink = await component.getByText('https://github.com/microsoft/playwright/issues/23181').first();
await expect(fourthLink).toBeVisible();
await expect(fourthLink).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/23181');
+});
+
+const resultWithAttachment: TestResult = {
+ retry: 0,
+ startTime: new Date(0).toUTCString(),
+ duration: 100,
+ errors: [],
+ steps: [{
+ title: 'Outer step',
+ startTime: new Date(100).toUTCString(),
+ duration: 10,
+ location: { file: 'test.spec.ts', line: 62, column: 0 },
+ count: 1,
+ steps: [],
+ }],
+ attachments: [{
+ name: 'first attachment',
+ body: 'The body with https://playwright.dev/docs/intro link and https://github.com/microsoft/playwright/issues/31284.',
+ contentType: 'text/plain'
+ }],
+ status: 'passed',
+};
+
+const attachmentLinkRenderingTestCase: TestCase = {
+ testId: 'testid',
+ title: 'My test',
+ path: [],
+ projectName: 'chromium',
+ location: { file: 'test.spec.ts', line: 42, column: 0 },
+ tags: [],
+ outcome: 'expected',
+ duration: 10,
+ ok: true,
+ annotations: [],
+ results: [resultWithAttachment]
+};
+
+test('should correctly render links in attachments', async ({ mount }) => {
+ const component = await mount();
+ await component.getByText('first attachment').click();
+ const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
+ await expect(body).toBeVisible();
+ await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro');
+ await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
});
\ No newline at end of file
diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx
index 507ddf4ffb..f4d76653cf 100644
--- a/packages/html-reporter/src/testCaseView.tsx
+++ b/packages/html-reporter/src/testCaseView.tsx
@@ -23,8 +23,8 @@ import { ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testCaseView.css';
import { TestResultView } from './testResultView';
-import { hashStringToInt } from './labelUtils';
-import { msToString } from './uiUtils';
+import { linkifyText } from './renderUtils';
+import { hashStringToInt, msToString } from './utils';
export const TestCaseView: React.FC<{
projectNames: string[],
@@ -68,43 +68,11 @@ export const TestCaseView: React.FC<{
;
};
-function renderAnnotationDescription(description: string) {
- const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f';
- const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug');
-
- const result = [];
- let currentIndex = 0;
- let match;
-
- while ((match = WEB_LINK_REGEX.exec(description)) !== null) {
- const stringBeforeMatch = description.substring(currentIndex, match.index);
- if (stringBeforeMatch)
- result.push(stringBeforeMatch);
-
- const value = match[0];
- result.push(renderLink(value));
- currentIndex = match.index + value.length;
- }
- const stringAfterMatches = description.substring(currentIndex);
- if (stringAfterMatches)
- result.push(stringAfterMatches);
-
- return result;
-}
-
-function renderLink(text: string) {
- let link = text;
- if (link.startsWith('www.'))
- link = 'https://' + link;
-
- return {text};
-}
-
function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) {
return (
{type}
- {description && : {renderAnnotationDescription(description)}}
+ {description && : {linkifyText(description)}}
);
}
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index 8e478f95d7..a5f9a7a358 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -16,14 +16,13 @@
import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
-import { msToString } from './uiUtils';
+import { hashStringToInt, msToString } from './utils';
import { Chip } from './chip';
import { filterWithToken, type Filter } from './filter';
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
import { statusIcon } from './statusIcon';
import './testFileView.css';
import { video, image, trace } from './icons';
-import { hashStringToInt } from './labelUtils';
export const TestFileView: React.FC