From de39d227f7a3a602c7b94a54f51e77b55557266d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 15 Jul 2024 12:20:22 -0700 Subject: [PATCH] chore: linkify urls in attachments body (#31673) Reference: https://github.com/microsoft/playwright/issues/31284 --- packages/html-reporter/src/labelUtils.tsx | 23 --------- packages/html-reporter/src/links.tsx | 5 +- packages/html-reporter/src/renderUtils.tsx | 47 ++++++++++++++++++ .../html-reporter/src/testCaseView.spec.tsx | 49 +++++++++++++++++-- packages/html-reporter/src/testCaseView.tsx | 38 ++------------ packages/html-reporter/src/testFileView.tsx | 3 +- packages/html-reporter/src/testFilesView.tsx | 2 +- packages/html-reporter/src/testResultView.tsx | 2 +- .../src/{uiUtils.ts => utils.ts} | 9 ++++ 9 files changed, 111 insertions(+), 67 deletions(-) delete mode 100644 packages/html-reporter/src/labelUtils.tsx create mode 100644 packages/html-reporter/src/renderUtils.tsx rename packages/html-reporter/src/{uiUtils.ts => utils.ts} (79%) 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