mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	feat(ui): "fix with ai" button (#34708)
This commit is contained in:
		
							parent
							
								
									2f8d448dbb
								
							
						
					
					
						commit
						0672f1ce67
					
				| @ -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); | ||||
|  | ||||
| @ -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>; | ||||
| }; | ||||
|  | ||||
| @ -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; | ||||
| @ -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' />; | ||||
| 
 | ||||
| @ -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>; | ||||
|  | ||||
| @ -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 = { | ||||
| @ -430,6 +431,7 @@ export const UIModeView: React.FC<{}> = ({ | ||||
|           <XtermWrapper source={xtermDataSource}></XtermWrapper> | ||||
|         </div> | ||||
|         <div className={clsx('vbox', isShowingOutput && 'hidden')}> | ||||
|           <GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}> | ||||
|             <TraceView | ||||
|               pathSeparator={queryParams.pathSeparator} | ||||
|               item={selectedItem} | ||||
| @ -437,6 +439,7 @@ export const UIModeView: React.FC<{}> = ({ | ||||
|               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'> | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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)'); | ||||
| }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Simon Knott
						Simon Knott