feat(ui): add error prompt button

This commit is contained in:
Simon Knott 2025-02-07 11:21:27 +01:00
parent 2f8d448dbb
commit 5e3f175c54
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
5 changed files with 42 additions and 8 deletions

View file

@ -36,6 +36,7 @@ test.describe('New Todo', () => {
await expect(page.getByTestId('todo-title')).toHaveText([ await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0], TODO_ITEMS[0],
TODO_ITEMS[1], TODO_ITEMS[1],
"faux"
]); ]);
await checkNumberOfTodosInLocalStorage(page, 2); await checkNumberOfTodosInLocalStorage(page, 2);

View file

@ -165,7 +165,7 @@ function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): b
return a.name === b.name && a.path === b.path && a.sha1 === b.sha1; return a.name === b.name && a.path === b.path && a.sha1 === b.sha1;
} }
function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) { export function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) {
const params = new URLSearchParams(queryParams); const params = new URLSearchParams(queryParams);
if (attachment.sha1) { if (attachment.sha1) {
params.set('trace', attachment.traceUrl); params.set('trace', attachment.traceUrl);

View file

@ -21,8 +21,9 @@ import './copyToClipboard.css';
export const CopyToClipboard: React.FunctionComponent<{ export const CopyToClipboard: React.FunctionComponent<{
value: string | (() => Promise<string>), value: string | (() => Promise<string>),
description?: string, description?: string,
}> = ({ value, description }) => { copyIcon?: string;
const [icon, setIcon] = React.useState('copy'); }> = ({ value, description, copyIcon = 'copy' }) => {
const [icon, setIcon] = React.useState(copyIcon);
const handleCopy = React.useCallback(() => { const handleCopy = React.useCallback(() => {
const valuePromise = typeof value === 'function' ? value() : Promise.resolve(value); const valuePromise = typeof value === 'function' ? value() : Promise.resolve(value);
@ -30,7 +31,7 @@ export const CopyToClipboard: React.FunctionComponent<{
navigator.clipboard.writeText(value).then(() => { navigator.clipboard.writeText(value).then(() => {
setIcon('check'); setIcon('check');
setTimeout(() => { setTimeout(() => {
setIcon('copy'); setIcon(copyIcon);
}, 3000); }, 3000);
}, () => { }, () => {
setIcon('close'); setIcon('close');
@ -39,8 +40,8 @@ export const CopyToClipboard: React.FunctionComponent<{
setIcon('close'); setIcon('close');
}); });
}, [value]); }, [value, copyIcon]);
return <ToolbarButton title={description ? description : 'Copy'} icon={icon} onClick={handleCopy}/>; return <ToolbarButton title={description ?? 'Copy'} icon={icon} onClick={handleCopy}/>;
}; };
export const CopyToClipboardTextButton: React.FunctionComponent<{ export const CopyToClipboardTextButton: React.FunctionComponent<{

View file

@ -21,6 +21,33 @@ import { PlaceholderPanel } from './placeholderPanel';
import { renderAction } from './actionList'; import { renderAction } from './actionList';
import type { Language } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators';
import type { StackFrame } from '@protocol/channels'; import type { StackFrame } from '@protocol/channels';
import { CopyToClipboard } from './copyToClipboard';
import { attachmentURL } from './attachmentsTab';
import { fixTestPrompt } from '@web/components/prompts';
const PromptButton: React.FC<{
error: string;
actions: modelUtil.ActionTraceEventInContext[];
}> = ({ error, actions }) => {
const [pageSnapshot, setPageSnapshot] = React.useState<string>();
React.useEffect(( )=> {
for (const action of actions) {
for (const attachment of action.attachments ?? []) {
if (attachment.name === 'pageSnapshot') {
fetch(attachmentURL({ ...attachment, traceUrl: action.context.traceUrl })).then(async response => {
setPageSnapshot(await response.text());
});
return;
}
}
}
}, [actions]);
const prompt = React.useMemo(() => fixTestPrompt(error, undefined, pageSnapshot), [error, pageSnapshot]);
return <CopyToClipboard value={prompt} copyIcon='copilot' description="Copy prompt to clipboard" />;
};
export type ErrorDescription = { export type ErrorDescription = {
action?: modelUtil.ActionTraceEventInContext; action?: modelUtil.ActionTraceEventInContext;
@ -44,9 +71,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
export const ErrorsTab: React.FunctionComponent<{ export const ErrorsTab: React.FunctionComponent<{
errorsModel: ErrorsTabModel, errorsModel: ErrorsTabModel,
actions: modelUtil.ActionTraceEventInContext[],
sdkLanguage: Language, sdkLanguage: Language,
revealInSource: (error: ErrorDescription) => void, revealInSource: (error: ErrorDescription) => void,
}> = ({ errorsModel, sdkLanguage, revealInSource }) => { }> = ({ errorsModel, sdkLanguage, revealInSource, actions }) => {
if (!errorsModel.errors.size) if (!errorsModel.errors.size)
return <PlaceholderPanel text='No errors' />; return <PlaceholderPanel text='No errors' />;
@ -72,7 +100,11 @@ export const ErrorsTab: React.FunctionComponent<{
{location && <div className='action-location'> {location && <div className='action-location'>
@ <span title={longLocation} onClick={() => revealInSource(error)}>{location}</span> @ <span title={longLocation} onClick={() => revealInSource(error)}>{location}</span>
</div>} </div>}
<span style={{ position: 'absolute', right: '20px' }}>
<PromptButton error={message} actions={actions} />
</span>
</div> </div>
<ErrorMessage error={message} /> <ErrorMessage error={message} />
</div>; </div>;
})} })}

View file

@ -199,7 +199,7 @@ export const Workbench: React.FunctionComponent<{
else else
setRevealedError(error); setRevealedError(error);
selectPropertiesTab('source'); selectPropertiesTab('source');
}} /> }} actions={model?.actions ?? []} />
}; };
// Fallback location w/o action stands for file / test. // Fallback location w/o action stands for file / test.