2021-12-12 23:56:12 +01:00
|
|
|
/*
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2024-12-16 15:25:32 +01:00
|
|
|
import type { TestAttachment, TestCase, TestCaseSummary, TestResult, TestResultSummary } from './types';
|
2021-12-12 23:56:12 +01:00
|
|
|
import * as React from 'react';
|
2021-12-14 00:37:01 +01:00
|
|
|
import * as icons from './icons';
|
2021-12-12 23:56:12 +01:00
|
|
|
import { TreeItem } from './treeItem';
|
2023-10-12 02:56:05 +02:00
|
|
|
import { CopyToClipboard } from './copyToClipboard';
|
2021-12-12 23:56:12 +01:00
|
|
|
import './links.css';
|
2024-08-01 12:43:29 +02:00
|
|
|
import { linkifyText } from '@web/renderUtils';
|
2025-01-22 19:06:07 +01:00
|
|
|
import { clsx, useFlash } from '@web/uiUtils';
|
2021-12-12 23:56:12 +01:00
|
|
|
|
2024-12-11 15:16:21 +01:00
|
|
|
export function navigate(href: string | URL) {
|
2021-12-14 00:37:01 +01:00
|
|
|
window.history.pushState({}, '', href);
|
|
|
|
|
const navEvent = new PopStateEvent('popstate');
|
|
|
|
|
window.dispatchEvent(navEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const Route: React.FunctionComponent<{
|
2022-07-12 04:47:15 +02:00
|
|
|
predicate: (params: URLSearchParams) => boolean,
|
2021-12-14 00:37:01 +01:00
|
|
|
children: any
|
2022-07-12 04:47:15 +02:00
|
|
|
}> = ({ predicate, children }) => {
|
2024-10-30 02:29:07 +01:00
|
|
|
const searchParams = React.useContext(SearchParamsContext);
|
|
|
|
|
return predicate(searchParams) ? children : null;
|
2021-12-14 00:37:01 +01:00
|
|
|
};
|
|
|
|
|
|
2021-12-12 23:56:12 +01:00
|
|
|
export const Link: React.FunctionComponent<{
|
2024-04-26 19:50:20 +02:00
|
|
|
href?: string,
|
|
|
|
|
click?: string,
|
|
|
|
|
ctrlClick?: string,
|
2021-12-12 23:56:12 +01:00
|
|
|
className?: string,
|
|
|
|
|
title?: string,
|
|
|
|
|
children: any,
|
2024-07-31 12:12:06 +02:00
|
|
|
}> = ({ click, ctrlClick, children, ...rest }) => {
|
|
|
|
|
return <a {...rest} style={{ textDecoration: 'none', color: 'var(--color-fg-default)', cursor: 'pointer' }} onClick={e => {
|
2024-04-26 19:50:20 +02:00
|
|
|
if (click) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
navigate(e.metaKey || e.ctrlKey ? ctrlClick || click : click);
|
|
|
|
|
}
|
|
|
|
|
}}>{children}</a>;
|
2021-12-12 23:56:12 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ProjectLink: React.FunctionComponent<{
|
2021-12-15 04:25:07 +01:00
|
|
|
projectNames: string[],
|
2021-12-12 23:56:12 +01:00
|
|
|
projectName: string,
|
2021-12-15 04:25:07 +01:00
|
|
|
}> = ({ projectNames, projectName }) => {
|
2021-12-12 23:56:12 +01:00
|
|
|
const encoded = encodeURIComponent(projectName);
|
|
|
|
|
const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`;
|
|
|
|
|
return <Link href={`#?q=p:${value}`}>
|
2024-07-31 12:12:06 +02:00
|
|
|
<span className={clsx('label', `label-color-${projectNames.indexOf(projectName) % 6}`)} style={{ margin: '6px 0 0 6px' }}>
|
2021-12-12 23:56:12 +01:00
|
|
|
{projectName}
|
|
|
|
|
</span>
|
|
|
|
|
</Link>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const AttachmentLink: React.FunctionComponent<{
|
|
|
|
|
attachment: TestAttachment,
|
2025-01-02 17:48:59 +01:00
|
|
|
result: TestResult,
|
2021-12-12 23:56:12 +01:00
|
|
|
href?: string,
|
2022-02-16 18:09:42 +01:00
|
|
|
linkName?: string,
|
2024-09-02 08:35:53 +02:00
|
|
|
openInNewTab?: boolean,
|
2025-01-02 17:48:59 +01:00
|
|
|
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
|
2025-01-22 19:06:07 +01:00
|
|
|
const [flash, triggerFlash] = useFlash();
|
|
|
|
|
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
|
2021-12-12 23:56:12 +01:00
|
|
|
return <TreeItem title={<span>
|
2021-12-14 00:37:01 +01:00
|
|
|
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
2023-08-16 18:06:04 +02:00
|
|
|
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
|
2024-09-02 08:35:53 +02:00
|
|
|
{!attachment.path && (
|
|
|
|
|
openInNewTab
|
|
|
|
|
? <a href={URL.createObjectURL(new Blob([attachment.body!], { type: attachment.contentType }))} target='_blank' rel='noreferrer' onClick={e => e.stopPropagation()}>{attachment.name}</a>
|
|
|
|
|
: <span>{linkifyText(attachment.name)}</span>
|
|
|
|
|
)}
|
2021-12-12 23:56:12 +01:00
|
|
|
</span>} loadChildren={attachment.body ? () => {
|
2024-08-20 14:16:28 +02:00
|
|
|
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
2025-01-22 19:06:07 +01:00
|
|
|
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
|
2021-12-12 23:56:12 +01:00
|
|
|
};
|
|
|
|
|
|
2024-10-30 02:29:07 +01:00
|
|
|
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
|
|
|
|
|
|
|
|
|
export const SearchParamsProvider: React.FunctionComponent<React.PropsWithChildren> = ({ children }) => {
|
|
|
|
|
const [searchParams, setSearchParams] = React.useState<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const listener = () => setSearchParams(new URLSearchParams(window.location.hash.slice(1)));
|
|
|
|
|
window.addEventListener('popstate', listener);
|
|
|
|
|
return () => window.removeEventListener('popstate', listener);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return <SearchParamsContext.Provider value={searchParams}>{children}</SearchParamsContext.Provider>;
|
|
|
|
|
};
|
|
|
|
|
|
2023-08-16 18:06:04 +02:00
|
|
|
function downloadFileNameForAttachment(attachment: TestAttachment): string {
|
|
|
|
|
if (attachment.name.includes('.') || !attachment.path)
|
|
|
|
|
return attachment.name;
|
|
|
|
|
const firstDotIndex = attachment.path.indexOf('.');
|
|
|
|
|
if (firstDotIndex === -1)
|
|
|
|
|
return attachment.name;
|
|
|
|
|
return attachment.name + attachment.path.slice(firstDotIndex, attachment.path.length);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-12 04:47:15 +02:00
|
|
|
export function generateTraceUrl(traces: TestAttachment[]) {
|
|
|
|
|
return `trace/index.html?${traces.map((a, i) => `trace=${new URL(a.path!, window.location.href)}`).join('&')}`;
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-12 23:56:12 +01:00
|
|
|
const kMissingContentType = 'x-playwright/missing';
|
2024-11-19 16:40:02 +01:00
|
|
|
|
2024-12-16 15:25:32 +01:00
|
|
|
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
|
2024-11-19 16:40:02 +01:00
|
|
|
|
2025-01-22 19:06:07 +01:00
|
|
|
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
|
2024-12-16 15:25:32 +01:00
|
|
|
const searchParams = React.useContext(SearchParamsContext);
|
|
|
|
|
const isAnchored = useIsAnchored(id);
|
2024-11-19 16:40:02 +01:00
|
|
|
React.useEffect(() => {
|
2024-12-16 15:25:32 +01:00
|
|
|
if (isAnchored)
|
2025-01-22 19:06:07 +01:00
|
|
|
return onReveal();
|
2024-12-16 15:25:32 +01:00
|
|
|
}, [isAnchored, onReveal, searchParams]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useIsAnchored(id: AnchorID) {
|
|
|
|
|
const searchParams = React.useContext(SearchParamsContext);
|
|
|
|
|
const anchor = searchParams.get('anchor');
|
|
|
|
|
if (anchor === null)
|
|
|
|
|
return false;
|
|
|
|
|
if (typeof id === 'undefined')
|
|
|
|
|
return false;
|
|
|
|
|
if (typeof id === 'string')
|
|
|
|
|
return id === anchor;
|
|
|
|
|
if (Array.isArray(id))
|
|
|
|
|
return id.includes(anchor);
|
|
|
|
|
return id(anchor);
|
2024-11-19 16:40:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
|
|
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
const onAnchorReveal = React.useCallback(() => {
|
2024-12-18 11:41:48 +01:00
|
|
|
ref.current?.scrollIntoView({ block: 'start', inline: 'start' });
|
2024-11-19 16:40:02 +01:00
|
|
|
}, []);
|
|
|
|
|
useAnchor(id, onAnchorReveal);
|
|
|
|
|
|
|
|
|
|
return <div ref={ref}>{children}</div>;
|
|
|
|
|
}
|
2024-12-16 15:25:32 +01:00
|
|
|
|
|
|
|
|
export function testResultHref({ test, result, anchor }: { test?: TestCase | TestCaseSummary, result?: TestResult | TestResultSummary, anchor?: string }) {
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
if (test)
|
|
|
|
|
params.set('testId', test.testId);
|
|
|
|
|
if (test && result)
|
|
|
|
|
params.set('run', '' + test.results.indexOf(result as any));
|
|
|
|
|
if (anchor)
|
|
|
|
|
params.set('anchor', anchor);
|
|
|
|
|
return `#?` + params;
|
|
|
|
|
}
|