add aria snapsht
This commit is contained in:
parent
3db114d96b
commit
14eed752dd
|
|
@ -20,12 +20,14 @@ import './copyToClipboard.css';
|
|||
|
||||
type CopyToClipboardProps = {
|
||||
value: string;
|
||||
icon?: JSX.Element;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A copy to clipboard button.
|
||||
*/
|
||||
export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {
|
||||
export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value, icon: copyIcon = icons.copy(), title }) => {
|
||||
type IconType = 'copy' | 'check' | 'cross';
|
||||
const [icon, setIcon] = React.useState<IconType>('copy');
|
||||
const handleCopy = React.useCallback(() => {
|
||||
|
|
@ -38,8 +40,8 @@ export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({
|
|||
setIcon('cross');
|
||||
});
|
||||
}, [value]);
|
||||
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
|
||||
return <button className='copy-icon' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : copyIcon;
|
||||
return <button className='copy-icon' aria-label='Copy to clipboard' title={title} onClick={handleCopy}>{iconElement}</button>;
|
||||
};
|
||||
|
||||
type CopyToClipboardContainerProps = CopyToClipboardProps & {
|
||||
|
|
|
|||
|
|
@ -97,3 +97,9 @@ export const copy = () => {
|
|||
<path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const copilot = () => {
|
||||
return <svg className='octicon' viewBox='0 0 48 48' version='1.1' width='20' height='20' aria-hidden='true'>
|
||||
<path d="M47.801 34.003c-1.72 2.988-11.706 10.037-23.82 10.037S1.881 36.991.161 34.003a1.309 1.309 0 0 1-.161-.57v-5.615c.012-.17.047-.338.11-.498.744-1.867 2.692-4.58 5.206-5.308.333-.855.826-2.106 1.287-3.029a20.112 20.112 0 0 1-.104-2.171c0-2.659.563-4.992 2.262-6.729.793-.811 1.777-1.433 2.945-1.901C14.502 5.911 18.483 4 23.938 4c5.455 0 9.523 1.911 12.319 4.182 1.167.468 2.151 1.09 2.944 1.901 1.699 1.737 2.263 4.07 2.263 6.729 0 .736-.027 1.465-.105 2.171.461.923.954 2.174 1.288 3.029 2.513.728 4.461 3.441 5.205 5.308.081.205.115.424.115.645v5.318c0 .252-.04.502-.166.72ZM24.325 22.031h-.688a8.52 8.52 0 0 1-.709 1.016c-1.537 1.892-3.833 2.98-7.008 2.98-3.447 0-5.972-.717-7.557-2.514a4.408 4.408 0 0 1-.171-.21l-.195.21v13.155c2.867 1.558 9.02 4.353 15.984 4.353s13.117-2.795 15.984-4.353V23.513l-.195-.21s-.066.091-.171.21c-1.584 1.797-4.11 2.514-7.557 2.514-3.175 0-5.47-1.088-7.008-2.98a8.637 8.637 0 0 1-.709-1.016h-.033.033Zm-1.969-5.864a14.31 14.31 0 0 0 .127-1.785v-.042c-.003-1.537-.339-2.538-.876-3.152-.681-.78-2.09-1.378-5.06-1.057-3.008.326-4.69 1.073-5.643 2.048-.923.944-1.408 2.356-1.408 4.633 0 2.42.348 3.849 1.115 4.719.729.827 2.165 1.499 5.309 1.499 2.417 0 3.799-.786 4.683-1.873.948-1.168 1.482-2.878 1.753-4.99Zm3.25 0c.271 2.112.805 3.822 1.754 4.99.883 1.087 2.265 1.873 4.682 1.873 3.145 0 4.58-.672 5.309-1.499.767-.87 1.116-2.299 1.116-4.719 0-2.277-.485-3.689-1.408-4.633-.954-.975-2.635-1.722-5.644-2.048-2.969-.321-4.378.277-5.06 1.057-.537.614-.873 1.615-.876 3.152v.042c.002.53.042 1.123.127 1.785Z"/><path d="M28.998 28.516c1.104 0 1.999.895 1.999 1.999v3.998a2 2 0 1 1-3.998 0v-3.998c0-1.104.895-1.999 1.999-1.999Zm-9.996 0c1.104 0 1.999.895 1.999 1.999v3.998a2 2 0 1 1-3.998 0v-3.998c0-1.104.895-1.999 1.999-1.999Z"/>
|
||||
</svg>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,18 +16,21 @@
|
|||
|
||||
@import '@web/third_party/vscode/colors.css';
|
||||
|
||||
.test-error-view {
|
||||
.test-error-container {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
line-height: initial;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.test-error-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.test-error-text {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,85 +17,58 @@
|
|||
import { ansi2html } from '@web/ansi2html';
|
||||
import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import * as icons from './icons';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { GitCommitInfoContext } from './reportView';
|
||||
import { TestResult } from './types';
|
||||
import { CopyToClipboard } from './copyToClipboard';
|
||||
|
||||
export const TestErrorView: React.FC<{
|
||||
error: string;
|
||||
testId?: string;
|
||||
hidePrompt?: boolean;
|
||||
}> = ({ error, testId, hidePrompt }) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
|
||||
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
|
||||
return (
|
||||
<>
|
||||
<div className='test-error-view test-error-text' data-testid={testId}>
|
||||
<div dangerouslySetInnerHTML={{ __html: html || '' }}></div>
|
||||
<CodeSnippet code={error} testId={testId}>
|
||||
<div style={{ float: 'right', padding: '5px' }}>
|
||||
<PromptButton error={error} result={result} />
|
||||
</div>
|
||||
</CodeSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<{ code: string; testId?: string; }>) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(code), [code]);
|
||||
return (
|
||||
<div className='test-error-container test-error-text' data-testid={testId}>
|
||||
{children}
|
||||
<div className='test-error-view' dangerouslySetInnerHTML={{ __html: html || '' }}></div>
|
||||
</div>
|
||||
<PromptButton error={error} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
|
||||
export function stripAnsiEscapes(str: string): string {
|
||||
function stripAnsiEscapes(str: string): string {
|
||||
return str.replace(ansiRegex, '');
|
||||
}
|
||||
|
||||
const PromptButton: React.FC<{
|
||||
error: string;
|
||||
}> = ({ error }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
result?: TestResult;
|
||||
}> = ({ error, result }) => {
|
||||
const gitCommitInfo = React.useContext(GitCommitInfoContext);
|
||||
if (!gitCommitInfo)
|
||||
return undefined;
|
||||
const prompt = React.useMemo(() => {
|
||||
const diff = gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'];
|
||||
const pageSnapshot = result?.attachments.find(a => a.name === 'pageSnapshot')?.body;
|
||||
|
||||
const diff = gitCommitInfo['pull.diff'] ?? gitCommitInfo['revision.diff'];
|
||||
if (!diff)
|
||||
return undefined;
|
||||
|
||||
const showFireworks = () => {
|
||||
const fireworksContainer = document.createElement('div');
|
||||
fireworksContainer.className = 'fireworks-container';
|
||||
document.body.appendChild(fireworksContainer);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(fireworksContainer);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
marginTop: '10px',
|
||||
borderRadius: '10px',
|
||||
border: '2px solid #4caf50',
|
||||
backgroundColor: copied ? '#4caf50' : '#fff',
|
||||
color: copied ? '#fff' : '#4caf50',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)',
|
||||
transition: 'background-color 0.3s, color 0.3s, transform 0.3s'
|
||||
}}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText([
|
||||
return [
|
||||
'You are a helpful assistant. Help me understand the error cause. Here is the error:',
|
||||
stripAnsiEscapes(error),
|
||||
'And this is the code diff:',
|
||||
diff
|
||||
].join('\n\n'));
|
||||
setCopied(true);
|
||||
showFireworks();
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy prompt to fix with AI'}
|
||||
</button>
|
||||
);
|
||||
diff,
|
||||
'And this is how the page looked:',
|
||||
pageSnapshot,
|
||||
].join('\n\n');
|
||||
}, [gitCommitInfo, result])
|
||||
|
||||
return <CopyToClipboard value={prompt} icon={<icons.copilot />} title="Copy prompt to clipboard" />;
|
||||
};
|
||||
|
||||
export const TestScreenshotErrorView: React.FC<{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Anchor, AttachmentLink, generateTraceUrl, testResultHref } from './link
|
|||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import { CodeSnippet, PromptButton, TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import './testResultView.css';
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ export const TestResultView: React.FC<{
|
|||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} result={result}></TestErrorView>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||
|
|
@ -182,7 +182,7 @@ const StepTreeItem: React.FC<{
|
|||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet} hidePrompt />] : [];
|
||||
const snippet = step.snippet ? [<CodeSnippet testId='test-snippet' key='line' code={step.snippet} />] : [];
|
||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
return snippet.concat(steps);
|
||||
} : undefined} depth={depth}/>;
|
||||
|
|
|
|||
|
|
@ -446,6 +446,17 @@ class HtmlBuilder {
|
|||
return a;
|
||||
}
|
||||
|
||||
if (a.name === 'pageSnapshot') {
|
||||
try {
|
||||
const body = fs.readFileSync(a.path!, { encoding: 'utf-8' });
|
||||
return {
|
||||
name: 'pageSnapshot',
|
||||
contentType: a.contentType,
|
||||
body,
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (a.path) {
|
||||
let fileName = a.path;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue