Skip to content

Commit d3422ec

Browse files
committed
Split linkVariants into its own file
1 parent ed9b6dd commit d3422ec

File tree

9 files changed

+352
-143
lines changed

9 files changed

+352
-143
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as React from 'react';
18+
import { SearchParamsContext } from './links';
19+
20+
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
21+
22+
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
23+
const searchParams = React.useContext(SearchParamsContext);
24+
const isAnchored = useIsAnchored(id);
25+
React.useEffect(() => {
26+
if (isAnchored)
27+
return onReveal();
28+
}, [isAnchored, onReveal, searchParams]);
29+
}
30+
31+
export function useIsAnchored(id: AnchorID) {
32+
const searchParams = React.useContext(SearchParamsContext);
33+
const anchor = searchParams.get('anchor');
34+
if (anchor === null)
35+
return false;
36+
if (typeof id === 'undefined')
37+
return false;
38+
if (typeof id === 'string')
39+
return id === anchor;
40+
if (Array.isArray(id))
41+
return id.includes(anchor);
42+
return id(anchor);
43+
}
44+
45+
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
46+
const ref = React.useRef<HTMLDivElement>(null);
47+
const onAnchorReveal = React.useCallback(() => {
48+
ref.current?.scrollIntoView({ block: 'start', inline: 'start' });
49+
}, []);
50+
useAnchor(id, onAnchorReveal);
51+
52+
return <div ref={ref}>{children}</div>;
53+
}

packages/html-reporter/src/chip.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import './colors.css';
2020
import './common.css';
2121
import * as icons from './icons';
2222
import { clsx } from '@web/uiUtils';
23-
import { type AnchorID, useAnchor } from './links';
23+
import { AnchorID, useAnchor } from './anchor';
2424

2525
export const Chip: React.FC<{
2626
header: JSX.Element | string,
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
Copyright (c) Microsoft Corporation.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.label {
18+
display: inline-block;
19+
padding: 0 8px;
20+
font-size: 12px;
21+
font-weight: 500;
22+
line-height: 18px;
23+
border: 1px solid transparent;
24+
border-radius: 2em;
25+
background-color: var(--color-scale-gray-4);
26+
color: white;
27+
margin: 0 10px;
28+
flex: none;
29+
font-weight: 600;
30+
}
31+
32+
@media(prefers-color-scheme: light) {
33+
.label-color-0 {
34+
background-color: var(--color-scale-blue-0);
35+
color: var(--color-scale-blue-6);
36+
border: 1px solid var(--color-scale-blue-4);
37+
}
38+
.label-color-1 {
39+
background-color: var(--color-scale-yellow-0);
40+
color: var(--color-scale-yellow-6);
41+
border: 1px solid var(--color-scale-yellow-4);
42+
}
43+
.label-color-2 {
44+
background-color: var(--color-scale-purple-0);
45+
color: var(--color-scale-purple-6);
46+
border: 1px solid var(--color-scale-purple-4);
47+
}
48+
.label-color-3 {
49+
background-color: var(--color-scale-pink-0);
50+
color: var(--color-scale-pink-6);
51+
border: 1px solid var(--color-scale-pink-4);
52+
}
53+
.label-color-4 {
54+
background-color: var(--color-scale-coral-0);
55+
color: var(--color-scale-coral-6);
56+
border: 1px solid var(--color-scale-coral-4);
57+
}
58+
.label-color-5 {
59+
background-color: var(--color-scale-orange-0);
60+
color: var(--color-scale-orange-6);
61+
border: 1px solid var(--color-scale-orange-4);
62+
}
63+
}
64+
65+
@media(prefers-color-scheme: dark) {
66+
.label-color-0 {
67+
background-color: var(--color-scale-blue-9);
68+
color: var(--color-scale-blue-2);
69+
border: 1px solid var(--color-scale-blue-4);
70+
}
71+
.label-color-1 {
72+
background-color: var(--color-scale-yellow-9);
73+
color: var(--color-scale-yellow-2);
74+
border: 1px solid var(--color-scale-yellow-4);
75+
}
76+
.label-color-2 {
77+
background-color: var(--color-scale-purple-9);
78+
color: var(--color-scale-purple-2);
79+
border: 1px solid var(--color-scale-purple-4);
80+
}
81+
.label-color-3 {
82+
background-color: var(--color-scale-pink-9);
83+
color: var(--color-scale-pink-2);
84+
border: 1px solid var(--color-scale-pink-4);
85+
}
86+
.label-color-4 {
87+
background-color: var(--color-scale-coral-9);
88+
color: var(--color-scale-coral-2);
89+
border: 1px solid var(--color-scale-coral-4);
90+
}
91+
.label-color-5 {
92+
background-color: var(--color-scale-orange-9);
93+
color: var(--color-scale-orange-2);
94+
border: 1px solid var(--color-scale-orange-4);
95+
}
96+
}
97+
98+
.attachment-body {
99+
white-space: pre-wrap;
100+
background-color: var(--color-canvas-subtle);
101+
margin-left: 24px;
102+
line-height: normal;
103+
padding: 8px;
104+
font-family: monospace;
105+
position: relative;
106+
}
107+
108+
.attachment-body .copy-icon {
109+
position: absolute;
110+
right: 5px;
111+
top: 5px;
112+
}
113+
114+
.link-badge {
115+
flex: none;
116+
background-color: transparent;
117+
border-color: transparent;
118+
}
119+
120+
.link-badge-dim span {
121+
color: var(--color-fg-muted);
122+
}
123+
124+
.link-badge:hover {
125+
cursor: pointer;
126+
}
127+
128+
.link-badge svg {
129+
fill: var(--color-fg-default);
130+
}
131+
132+
.link-badge-dim svg {
133+
fill: var(--color-fg-muted);
134+
}
135+
136+
.link-badge-dim:hover svg {
137+
fill: var(--color-fg-muted);
138+
}
139+
140+
.link-trace {
141+
/* Trace link button has 1px border and 4px padding, so we need 3px right margin to match content on the other side of divider */
142+
margin-right: 3px;
143+
}
144+
145+
.link-trace-separator {
146+
color: var(--color-fg-muted);
147+
user-select: none;
148+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as React from 'react';
18+
import { Link, LinkProps } from './links';
19+
import { clsx, useFlash } from '@web/uiUtils';
20+
import type { TestAttachment, TestCaseSummary, TestResult } from './types';
21+
import { TreeItem } from './treeItem';
22+
import * as icons from './icons';
23+
import { useAnchor } from './anchor';
24+
import { linkifyText } from '@web/renderUtils';
25+
import { CopyToClipboard } from './copyToClipboard';
26+
import { generateTraceUrl } from './url';
27+
import './colors.css';
28+
import './linkVariants.css';
29+
30+
export const LinkBadge: React.FunctionComponent<LinkProps & { dim?: boolean }> = ({ className, ...props }) => <Link {...props} className={clsx('link-badge', props.dim && 'link-badge-dim', className)} />;
31+
32+
export const ProjectLink: React.FunctionComponent<{
33+
projectNames: string[],
34+
projectName: string,
35+
}> = ({ projectNames, projectName }) => {
36+
const encoded = encodeURIComponent(projectName);
37+
const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`;
38+
return <Link href={`#?q=p:${value}`}>
39+
<span className={clsx('label', `label-color-${projectNames.indexOf(projectName) % 6}`)} style={{ margin: '6px 0 0 6px' }}>
40+
{projectName}
41+
</span>
42+
</Link>;
43+
};
44+
45+
const kMissingContentType = 'x-playwright/missing';
46+
47+
export const AttachmentLink: React.FunctionComponent<{
48+
attachment: TestAttachment,
49+
result: TestResult,
50+
href?: string,
51+
linkName?: string,
52+
openInNewTab?: boolean,
53+
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
54+
const [flash, triggerFlash] = useFlash();
55+
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
56+
return <TreeItem title={<span>
57+
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
58+
{attachment.path && (
59+
openInNewTab
60+
? <a href={href || attachment.path} target='_blank' rel='noreferrer'>{linkName || attachment.name}</a>
61+
: <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>
62+
)}
63+
{!attachment.path && (
64+
openInNewTab
65+
? (
66+
<a
67+
href={URL.createObjectURL(new Blob([attachment.body!], { type: attachment.contentType }))}
68+
target='_blank' rel='noreferrer'
69+
onClick={e => e.stopPropagation() /* dont expand the tree item */}
70+
>
71+
{attachment.name}
72+
</a>
73+
)
74+
: <span>{linkifyText(attachment.name)}</span>
75+
)}
76+
</span>} loadChildren={attachment.body ? () => {
77+
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
78+
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
79+
};
80+
81+
export const TraceLink: React.FC<{ test: TestCaseSummary, trailingSeparator?: boolean, dim?: boolean }> = ({ test, trailingSeparator, dim }) => {
82+
const firstTraces = test.results.map(result => result.attachments.filter(attachment => attachment.name === 'trace')).filter(traces => traces.length > 0)[0];
83+
if (!firstTraces)
84+
return undefined;
85+
86+
return (
87+
<>
88+
<LinkBadge
89+
href={generateTraceUrl(firstTraces)}
90+
title='View Trace'
91+
className='button link-trace'
92+
dim={dim}>
93+
{icons.trace()}
94+
<span>View Trace</span>
95+
</LinkBadge>
96+
{trailingSeparator && <div className='link-trace-separator'>|</div>}
97+
</>
98+
);
99+
};
100+
101+
function downloadFileNameForAttachment(attachment: TestAttachment): string {
102+
if (attachment.name.includes('.') || !attachment.path)
103+
return attachment.name;
104+
const firstDotIndex = attachment.path.indexOf('.');
105+
if (firstDotIndex === -1)
106+
return attachment.name;
107+
return attachment.name + attachment.path.slice(firstDotIndex, attachment.path.length);
108+
}

0 commit comments

Comments
 (0)