Skip to content

Commit

Permalink
feat(ui): "fix with ai" button (#34708)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skn0tt authored Feb 10, 2025
1 parent 2f8d448 commit 0672f1c
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/attachmentsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): b
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);
if (attachment.sha1) {
params.set('trace', attachment.traceUrl);
Expand Down
9 changes: 7 additions & 2 deletions packages/trace-viewer/src/ui/copyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ export const CopyToClipboard: React.FunctionComponent<{
export const CopyToClipboardTextButton: React.FunctionComponent<{
value: string | (() => Promise<string>),
description: string,
}> = ({ value, description }) => {
copiedDescription?: React.ReactNode,
style?: React.CSSProperties,
}> = ({ value, description, copiedDescription = description, style }) => {
const [copied, setCopied] = React.useState(false);
const handleCopy = React.useCallback(async () => {
const valueToCopy = typeof value === 'function' ? await value() : value;
await navigator.clipboard.writeText(valueToCopy);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
}, [value]);

return <ToolbarButton title={description} onClick={handleCopy} className='copy-to-clipboard-text-button'>{description}</ToolbarButton>;
return <ToolbarButton style={style} title={description} onClick={handleCopy} className='copy-to-clipboard-text-button'>{copied ? copiedDescription : description}</ToolbarButton>;
};
59 changes: 58 additions & 1 deletion packages/trace-viewer/src/ui/errorsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,59 @@ import { PlaceholderPanel } from './placeholderPanel';
import { renderAction } from './actionList';
import type { Language } from '@isomorphic/locatorGenerators';
import type { StackFrame } from '@protocol/channels';
import { CopyToClipboardTextButton } from './copyToClipboard';
import { attachmentURL } from './attachmentsTab';
import { fixTestPrompt } from '@web/components/prompts';
import type { GitCommitInfo } from '@testIsomorphic/types';

const GitCommitInfoContext = React.createContext<GitCommitInfo | undefined>(undefined);

export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) {
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
}

export function useGitCommitInfo() {
return React.useContext(GitCommitInfoContext);
}

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 gitCommitInfo = useGitCommitInfo();
const prompt = React.useMemo(
() => fixTestPrompt(
error,
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
pageSnapshot
),
[error, gitCommitInfo, pageSnapshot]
);

return (
<CopyToClipboardTextButton
value={prompt}
description='Fix with AI'
copiedDescription={<>Copied <span className='codicon codicon-copy' style={{ marginLeft: '5px' }}/></>}
style={{ width: '90px', justifyContent: 'center' }}
/>
);
};

export type ErrorDescription = {
action?: modelUtil.ActionTraceEventInContext;
Expand All @@ -44,9 +97,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):

export const ErrorsTab: React.FunctionComponent<{
errorsModel: ErrorsTabModel,
actions: modelUtil.ActionTraceEventInContext[],
sdkLanguage: Language,
revealInSource: (error: ErrorDescription) => void,
}> = ({ errorsModel, sdkLanguage, revealInSource }) => {
}> = ({ errorsModel, sdkLanguage, revealInSource, actions }) => {
if (!errorsModel.errors.size)
return <PlaceholderPanel text='No errors' />;

Expand All @@ -72,6 +126,9 @@ export const ErrorsTab: React.FunctionComponent<{
{location && <div className='action-location'>
@ <span title={longLocation} onClick={() => revealInSource(error)}>{location}</span>
</div>}
<span style={{ position: 'absolute', right: '5px' }}>
<PromptButton error={message} actions={actions} />
</span>
</div>
<ErrorMessage error={message} />
</div>;
Expand Down
17 changes: 10 additions & 7 deletions packages/trace-viewer/src/ui/uiModeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { TestListView } from './uiModeTestListView';
import { TraceView } from './uiModeTraceView';
import { SettingsView } from './settingsView';
import { DefaultSettingsView } from './defaultSettingsView';
import { GitCommitInfoProvider } from './errorsTab';

let xtermSize = { cols: 80, rows: 24 };
const xtermDataSource: XtermDataSource = {
Expand Down Expand Up @@ -430,13 +431,15 @@ export const UIModeView: React.FC<{}> = ({
<XtermWrapper source={xtermDataSource}></XtermWrapper>
</div>
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
<TraceView
pathSeparator={queryParams.pathSeparator}
item={selectedItem}
rootDir={testModel?.config?.rootDir}
revealSource={revealSource}
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
/>
<GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}>
<TraceView
pathSeparator={queryParams.pathSeparator}
item={selectedItem}
rootDir={testModel?.config?.rootDir}
revealSource={revealSource}
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
/>
</GitCommitInfoProvider>
</div>
</div>}
sidebar={<div className='vbox ui-mode-sidebar'>
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export const Workbench: React.FunctionComponent<{
else
setRevealedError(error);
selectPropertiesTab('source');
}} />
}} actions={model?.actions ?? []} />
};

// Fallback location w/o action stands for file / test.
Expand Down
19 changes: 19 additions & 0 deletions tests/playwright-test/ui-mode-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,22 @@ test('skipped steps should have an indicator', async ({ runUITest }) => {
await expect(skippedMarker).toBeVisible();
await expect(skippedMarker).toHaveAccessibleName('skipped');
});

test('should show copy prompt button in errors tab', async ({ runUITest }) => {
const { page } = await runUITest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('fails', async () => {
expect(1).toBe(2);
});
`,
});

await page.getByText('fails').dblclick();

await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
await page.getByText('Errors', { exact: true }).click();
await page.locator('.tab-errors').getByRole('button', { name: 'Fix with AI' }).click();
const prompt = await page.evaluate(() => navigator.clipboard.readText());
expect(prompt, 'contains error').toContain('expect(received).toBe(expected)');
});

0 comments on commit 0672f1c

Please sign in to comment.