add aria snapsht

This commit is contained in:
Simon Knott 2025-02-07 10:13:54 +01:00
parent 3db114d96b
commit 14eed752dd
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
6 changed files with 64 additions and 69 deletions

View file

@ -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 & {

View file

@ -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>;
};

View file

@ -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;
}

View file

@ -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<{

View file

@ -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}/>;

View file

@ -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 {