feat(ui mode): linkify attachment names and content (#31960)
- Pass `contentType` to the CodeMirror. - Support `text/markdown` mode. - Custom mode for non-supported types that linkifies urls.
This commit is contained in:
parent
76cca7fc2c
commit
a541751657
|
|
@ -23,6 +23,7 @@ import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||||
import { Expandable } from '@web/components/expandable';
|
import { Expandable } from '@web/components/expandable';
|
||||||
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||||
|
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (expanded && attachmentText === null && placeholder === null) {
|
if (expanded && attachmentText === null && placeholder === null) {
|
||||||
|
|
@ -50,10 +52,10 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
}, [expanded, attachmentText, placeholder, attachment]);
|
}, [expanded, attachmentText, placeholder, attachment]);
|
||||||
|
|
||||||
const title = <span style={{ marginLeft: 5 }}>
|
const title = <span style={{ marginLeft: 5 }}>
|
||||||
{attachment.name} <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>
|
{linkifyText(attachment.name)} {hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
if (!isTextAttachment)
|
if (!isTextAttachment || !hasContent)
|
||||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
@ -63,6 +65,8 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
{expanded && attachmentText !== null && <CodeMirrorWrapper
|
{expanded && attachmentText !== null && <CodeMirrorWrapper
|
||||||
text={attachmentText}
|
text={attachmentText}
|
||||||
readOnly
|
readOnly
|
||||||
|
mimeType={attachment.contentType}
|
||||||
|
linkify={true}
|
||||||
lineNumbers={true}
|
lineNumbers={true}
|
||||||
wrapLines={false}>
|
wrapLines={false}>
|
||||||
</CodeMirrorWrapper>}
|
</CodeMirrorWrapper>}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import * as React from 'react';
|
||||||
import './networkResourceDetails.css';
|
import './networkResourceDetails.css';
|
||||||
import { TabbedPane } from '@web/components/tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import type { Language } from '@web/components/codeMirrorWrapper';
|
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
|
||||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
|
|
@ -55,19 +54,18 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
const RequestTab: React.FunctionComponent<{
|
const RequestTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
}> = ({ resource }) => {
|
}> = ({ resource }) => {
|
||||||
const [requestBody, setRequestBody] = React.useState<{ text: string, language?: Language } | null>(null);
|
const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
if (resource.request.postData) {
|
if (resource.request.postData) {
|
||||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
||||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
||||||
const language = mimeTypeToHighlighter(requestContentType);
|
|
||||||
if (resource.request.postData._sha1) {
|
if (resource.request.postData._sha1) {
|
||||||
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
||||||
setRequestBody({ text: formatBody(await response.text(), requestContentType), language });
|
setRequestBody({ text: formatBody(await response.text(), requestContentType), mimeType: requestContentType });
|
||||||
} else {
|
} else {
|
||||||
setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), language });
|
setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), mimeType: requestContentType });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setRequestBody(null);
|
setRequestBody(null);
|
||||||
|
|
@ -87,7 +85,7 @@ const RequestTab: React.FunctionComponent<{
|
||||||
<div className='network-request-details-header'>Request Headers</div>
|
<div className='network-request-details-header'>Request Headers</div>
|
||||||
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||||
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||||
{requestBody && <CodeMirrorWrapper text={requestBody.text} language={requestBody.language} readOnly lineNumbers={true}/>}
|
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -103,7 +101,7 @@ const ResponseTab: React.FunctionComponent<{
|
||||||
const BodyTab: React.FunctionComponent<{
|
const BodyTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
}> = ({ resource }) => {
|
}> = ({ resource }) => {
|
||||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, language?: Language } | null>(null);
|
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
|
|
@ -118,8 +116,7 @@ const BodyTab: React.FunctionComponent<{
|
||||||
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
||||||
} else {
|
} else {
|
||||||
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
||||||
const language = mimeTypeToHighlighter(resource.response.content.mimeType);
|
setResponseBody({ text: formattedBody, mimeType: resource.response.content.mimeType });
|
||||||
setResponseBody({ text: formattedBody, language });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -130,7 +127,7 @@ const BodyTab: React.FunctionComponent<{
|
||||||
return <div className='network-request-details-tab'>
|
return <div className='network-request-details-tab'>
|
||||||
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
||||||
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||||
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} language={responseBody.language} readOnly lineNumbers={true}/>}
|
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -163,12 +160,3 @@ function formatBody(body: string | null, contentType: string): string {
|
||||||
|
|
||||||
return bodyStr;
|
return bodyStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mimeTypeToHighlighter(mimeType: string): Language | undefined {
|
|
||||||
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
|
||||||
return 'javascript';
|
|
||||||
if (mimeType.includes('html'))
|
|
||||||
return 'html';
|
|
||||||
if (mimeType.includes('css'))
|
|
||||||
return 'css';
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
|
||||||
import 'codemirror-shadow-1/mode/javascript/javascript';
|
import 'codemirror-shadow-1/mode/javascript/javascript';
|
||||||
import 'codemirror-shadow-1/mode/python/python';
|
import 'codemirror-shadow-1/mode/python/python';
|
||||||
import 'codemirror-shadow-1/mode/clike/clike';
|
import 'codemirror-shadow-1/mode/clike/clike';
|
||||||
|
import 'codemirror-shadow-1/mode/markdown/markdown';
|
||||||
|
import 'codemirror-shadow-1/addon/mode/simple';
|
||||||
|
|
||||||
export type CodeMirror = typeof codemirrorType;
|
export type CodeMirror = typeof codemirrorType;
|
||||||
export default codemirror;
|
export default codemirror;
|
||||||
|
|
|
||||||
|
|
@ -174,3 +174,9 @@ body.dark-mode .CodeMirror span.cm-type {
|
||||||
margin: 3px 10px;
|
margin: 3px 10px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror span.cm-link, span.cm-linkified {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import './codeMirrorWrapper.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { CodeMirror } from './codeMirrorModule';
|
import type { CodeMirror } from './codeMirrorModule';
|
||||||
import { ansi2html } from '../ansi2html';
|
import { ansi2html } from '../ansi2html';
|
||||||
import { useMeasure } from '../uiUtils';
|
import { useMeasure, kWebLinkRe } from '../uiUtils';
|
||||||
|
|
||||||
export type SourceHighlight = {
|
export type SourceHighlight = {
|
||||||
line: number;
|
line: number;
|
||||||
|
|
@ -26,11 +26,13 @@ export type SourceHighlight = {
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css' | 'markdown';
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string;
|
text: string;
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
mimeType?: string;
|
||||||
|
linkify?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
// 1-based
|
// 1-based
|
||||||
highlight?: SourceHighlight[];
|
highlight?: SourceHighlight[];
|
||||||
|
|
@ -45,6 +47,8 @@ export interface SourceProps {
|
||||||
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
text,
|
text,
|
||||||
language,
|
language,
|
||||||
|
mimeType,
|
||||||
|
linkify,
|
||||||
readOnly,
|
readOnly,
|
||||||
highlight,
|
highlight,
|
||||||
revealLine,
|
revealLine,
|
||||||
|
|
@ -63,24 +67,13 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
(async () => {
|
(async () => {
|
||||||
// Always load the module first.
|
// Always load the module first.
|
||||||
const CodeMirror = await modulePromise;
|
const CodeMirror = await modulePromise;
|
||||||
|
defineCustomMode(CodeMirror);
|
||||||
|
|
||||||
const element = codemirrorElement.current;
|
const element = codemirrorElement.current;
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let mode = '';
|
const mode = languageToMode(language) || mimeTypeToMode(mimeType) || (linkify ? 'text/linkified' : '');
|
||||||
if (language === 'javascript')
|
|
||||||
mode = 'javascript';
|
|
||||||
if (language === 'python')
|
|
||||||
mode = 'python';
|
|
||||||
if (language === 'java')
|
|
||||||
mode = 'text/x-java';
|
|
||||||
if (language === 'csharp')
|
|
||||||
mode = 'text/x-csharp';
|
|
||||||
if (language === 'html')
|
|
||||||
mode = 'htmlmixed';
|
|
||||||
if (language === 'css')
|
|
||||||
mode = 'css';
|
|
||||||
|
|
||||||
if (codemirrorRef.current
|
if (codemirrorRef.current
|
||||||
&& mode === codemirrorRef.current.cm.getOption('mode')
|
&& mode === codemirrorRef.current.cm.getOption('mode')
|
||||||
|
|
@ -106,7 +99,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
setCodemirror(cm);
|
setCodemirror(cm);
|
||||||
return cm;
|
return cm;
|
||||||
})();
|
})();
|
||||||
}, [modulePromise, codemirror, codemirrorElement, language, lineNumbers, wrapLines, readOnly, isFocused]);
|
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (codemirrorRef.current)
|
if (codemirrorRef.current)
|
||||||
|
|
@ -175,5 +168,69 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
};
|
};
|
||||||
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
||||||
|
|
||||||
return <div className='cm-wrapper' ref={codemirrorElement}></div>;
|
return <div className='cm-wrapper' ref={codemirrorElement} onClick={onCodeMirrorClick}></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onCodeMirrorClick(event: React.MouseEvent) {
|
||||||
|
if (!(event.target instanceof HTMLElement))
|
||||||
|
return;
|
||||||
|
let url: string | undefined;
|
||||||
|
if (event.target.classList.contains('cm-linkified')) {
|
||||||
|
// 'text/linkified' custom mode
|
||||||
|
url = event.target.textContent!;
|
||||||
|
} else if (event.target.classList.contains('cm-link') && event.target.nextElementSibling?.classList.contains('cm-url')) {
|
||||||
|
// 'markdown' mode
|
||||||
|
url = event.target.nextElementSibling.textContent!.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let customModeDefined = false;
|
||||||
|
function defineCustomMode(cm: CodeMirror) {
|
||||||
|
if (customModeDefined)
|
||||||
|
return;
|
||||||
|
customModeDefined = true;
|
||||||
|
(cm as any).defineSimpleMode('text/linkified', {
|
||||||
|
start: [
|
||||||
|
{ regex: kWebLinkRe, token: 'linkified' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mimeTypeToMode(mimeType: string | undefined): string | undefined {
|
||||||
|
if (!mimeType)
|
||||||
|
return;
|
||||||
|
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
||||||
|
return 'javascript';
|
||||||
|
if (mimeType.includes('python'))
|
||||||
|
return 'python';
|
||||||
|
if (mimeType.includes('csharp'))
|
||||||
|
return 'text/x-csharp';
|
||||||
|
if (mimeType.includes('java'))
|
||||||
|
return 'text/x-java';
|
||||||
|
if (mimeType.includes('markdown'))
|
||||||
|
return 'markdown';
|
||||||
|
if (mimeType.includes('html') || mimeType.includes('svg'))
|
||||||
|
return 'htmlmixed';
|
||||||
|
if (mimeType.includes('css'))
|
||||||
|
return 'css';
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageToMode(language: Language | undefined): string | undefined {
|
||||||
|
if (!language)
|
||||||
|
return;
|
||||||
|
return {
|
||||||
|
javascript: 'javascript',
|
||||||
|
jsonl: 'javascript',
|
||||||
|
python: 'python',
|
||||||
|
csharp: 'text/x-csharp',
|
||||||
|
java: 'text/x-java',
|
||||||
|
markdown: 'markdown',
|
||||||
|
html: 'htmlmixed',
|
||||||
|
css: 'css',
|
||||||
|
}[language];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function linkifyText(description: string) {
|
import { kWebLinkRe } from './uiUtils';
|
||||||
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');
|
|
||||||
|
|
||||||
|
export function linkifyText(description: string) {
|
||||||
const result = [];
|
const result = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = WEB_LINK_REGEX.exec(description)) !== null) {
|
while ((match = kWebLinkRe.exec(description)) !== null) {
|
||||||
const stringBeforeMatch = description.substring(currentIndex, match.index);
|
const stringBeforeMatch = description.substring(currentIndex, match.index);
|
||||||
if (stringBeforeMatch)
|
if (stringBeforeMatch)
|
||||||
result.push(stringBeforeMatch);
|
result.push(stringBeforeMatch);
|
||||||
|
|
|
||||||
|
|
@ -196,3 +196,6 @@ export const settings = new Settings();
|
||||||
export function clsx(...classes: (string | undefined | false)[]) {
|
export function clsx(...classes: (string | undefined | false)[]) {
|
||||||
return classes.filter(Boolean).join(' ');
|
return classes.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||||
|
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,55 @@ test('should contain string attachment', async ({ runUITest }) => {
|
||||||
expect((await readAllFromStream(await download.createReadStream())).toString()).toEqual('text42');
|
expect((await readAllFromStream(await download.createReadStream())).toString()).toEqual('text42');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should linkify string attachments', async ({ runUITest, server }) => {
|
||||||
|
server.setRoute('/one.html', (req, res) => res.end());
|
||||||
|
server.setRoute('/two.html', (req, res) => res.end());
|
||||||
|
server.setRoute('/three.html', (req, res) => res.end());
|
||||||
|
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test } from '@playwright/test';
|
||||||
|
test('attach test', async () => {
|
||||||
|
await test.info().attach('Inline url: ${server.PREFIX + '/one.html'}');
|
||||||
|
await test.info().attach('Second', { body: 'Inline link ${server.PREFIX + '/two.html'} to be highlighted.' });
|
||||||
|
await test.info().attach('Third', { body: '[markdown link](${server.PREFIX + '/three.html'})', contentType: 'text/markdown' });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
await page.getByText('attach test').click();
|
||||||
|
await page.getByTitle('Run all').click();
|
||||||
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
|
await page.getByText('Attachments').click();
|
||||||
|
|
||||||
|
const attachmentsPane = page.locator('.attachments-tab');
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = server.PREFIX + '/one.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText(url).click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await attachmentsPane.getByText('Second download').click();
|
||||||
|
const url = server.PREFIX + '/two.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText(url).click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await attachmentsPane.getByText('Third download').click();
|
||||||
|
const url = server.PREFIX + '/three.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText('[markdown link]').click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue