mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: linkify urls in attachments body (#31673)
Reference: https://github.com/microsoft/playwright/issues/31284
This commit is contained in:
parent
d463d1f285
commit
de39d227f7
@ -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);
|
||||
}
|
||||
@ -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 <TreeItem title={<span>
|
||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
||||
{attachment.body && <span>{attachment.name}</span>}
|
||||
{attachment.body && <span>{linkifyText(attachment.name)}</span>}
|
||||
</span>} loadChildren={attachment.body ? () => {
|
||||
return [<div className='attachment-body'><CopyToClipboard value={attachment.body!}/>{attachment.body}</div>];
|
||||
return [<div className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||
};
|
||||
|
||||
|
||||
47
packages/html-reporter/src/renderUtils.tsx
Normal file
47
packages/html-reporter/src/renderUtils.tsx
Normal file
@ -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 <a href={link} target='_blank' rel='noopener noreferrer'>{text}</a>;
|
||||
}
|
||||
@ -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(<TestCaseView projectNames={['chromium', 'webkit']} test={linkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
// const container = await(component.getByText('Annotations'));
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
|
||||
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(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
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');
|
||||
});
|
||||
@ -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<{
|
||||
</div>;
|
||||
};
|
||||
|
||||
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 <a href={link} target='_blank' rel='noopener noreferrer'>{text}</a>;
|
||||
}
|
||||
|
||||
function TestCaseAnnotationView({ annotation: { type, description } }: { annotation: TestCaseAnnotation }) {
|
||||
return (
|
||||
<div className='test-case-annotation'>
|
||||
<span style={{ fontWeight: 'bold' }}>{type}</span>
|
||||
{description && <span>: {renderAnnotationDescription(description)}</span>}
|
||||
{description && <span>: {linkifyText(description)}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<React.PropsWithChildren<{
|
||||
report: HTMLReport;
|
||||
|
||||
@ -19,7 +19,7 @@ import * as React from 'react';
|
||||
import type { Filter } from './filter';
|
||||
import { TestFileView } from './testFileView';
|
||||
import './testFileView.css';
|
||||
import { msToString } from './uiUtils';
|
||||
import { msToString } from './utils';
|
||||
import { AutoChip } from './chip';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
import type { TestAttachment, TestCase, TestResult, TestStep } from './types';
|
||||
import * as React from 'react';
|
||||
import { TreeItem } from './treeItem';
|
||||
import { msToString } from './uiUtils';
|
||||
import { msToString } from './utils';
|
||||
import { AutoChip } from './chip';
|
||||
import { traceImage } from './images';
|
||||
import { AttachmentLink, generateTraceUrl } from './links';
|
||||
|
||||
@ -39,3 +39,12 @@ export function msToString(ms: number): string {
|
||||
const days = hours / 24;
|
||||
return days.toFixed(1) + 'd';
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user