mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: refactor error context (#35613)
This commit is contained in:
		
							parent
							
								
									78600c60f8
								
							
						
					
					
						commit
						cb2d94e467
					
				| @ -20,17 +20,18 @@ import './testErrorView.css'; | |||||||
| import type { ImageDiff } from '@web/shared/imageDiffView'; | import type { ImageDiff } from '@web/shared/imageDiffView'; | ||||||
| import { ImageDiffView } from '@web/shared/imageDiffView'; | import { ImageDiffView } from '@web/shared/imageDiffView'; | ||||||
| import { TestAttachment } from './types'; | import { TestAttachment } from './types'; | ||||||
|  | import { fixTestInstructions } from '@web/prompts'; | ||||||
| 
 | 
 | ||||||
| export const TestErrorView: React.FC<{ | export const TestErrorView: React.FC<{ | ||||||
|   error: string; |   error: string; | ||||||
|   testId?: string; |   testId?: string; | ||||||
|   prompt?: TestAttachment; |   context?: TestAttachment; | ||||||
| }> = ({ error, testId, prompt }) => { | }> = ({ error, testId, context }) => { | ||||||
|   return ( |   return ( | ||||||
|     <CodeSnippet code={error} testId={testId}> |     <CodeSnippet code={error} testId={testId}> | ||||||
|       {prompt && ( |       {context && ( | ||||||
|         <div style={{ position: 'absolute', right: 0, padding: '10px' }}> |         <div style={{ position: 'absolute', right: 0, padding: '10px' }}> | ||||||
|           <PromptButton prompt={prompt} /> |           <PromptButton context={context} /> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
|     </CodeSnippet> |     </CodeSnippet> | ||||||
| @ -47,14 +48,14 @@ export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren< | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const PromptButton: React.FC<{ prompt: TestAttachment }> = ({ prompt }) => { | const PromptButton: React.FC<{ context: TestAttachment }> = ({ context }) => { | ||||||
|   const [copied, setCopied] = React.useState(false); |   const [copied, setCopied] = React.useState(false); | ||||||
|   return <button |   return <button | ||||||
|     className='button' |     className='button' | ||||||
|     style={{ minWidth: 100 }} |     style={{ minWidth: 100 }} | ||||||
|     onClick={async () => { |     onClick={async () => { | ||||||
|       const text = prompt.body ? prompt.body : await fetch(prompt.path!).then(r => r.text()); |       const text = context.body ? context.body : await fetch(context.path!).then(r => r.text()); | ||||||
|       await navigator.clipboard.writeText(text); |       await navigator.clipboard.writeText(fixTestInstructions + text); | ||||||
|       setCopied(true); |       setCopied(true); | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|         setCopied(false); |         setCopied(false); | ||||||
|  | |||||||
| @ -90,7 +90,7 @@ export const TestResultView: React.FC<{ | |||||||
|       {errors.map((error, index) => { |       {errors.map((error, index) => { | ||||||
|         if (error.type === 'screenshot') |         if (error.type === 'screenshot') | ||||||
|           return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>; |           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!} prompt={error.prompt}></TestErrorView>; |         return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} context={error.context}></TestErrorView>; | ||||||
|       })} |       })} | ||||||
|     </AutoChip>} |     </AutoChip>} | ||||||
|     {!!result.steps.length && <AutoChip header='Test Steps'> |     {!!result.steps.length && <AutoChip header='Test Steps'> | ||||||
| @ -165,8 +165,8 @@ function classifyErrors(testErrors: string[], diffs: ImageDiff[], attachments: T | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const prompt = attachments.find(a => a.name === `_prompt-${i}`); |     const context = attachments.find(a => a.name === `_error-context-${i}`); | ||||||
|     return { type: 'regular', error, prompt }; |     return { type: 'regular', error, context }; | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,5 +14,5 @@ common/ | |||||||
| [internalsForTest.ts] | [internalsForTest.ts] | ||||||
| ** | ** | ||||||
| 
 | 
 | ||||||
| [prompt.ts] | [errorContext.ts] | ||||||
| ./transform/babelBundle.ts | ./transform/babelBundle.ts | ||||||
|  | |||||||
| @ -22,14 +22,25 @@ import { parseErrorStack } from 'playwright-core/lib/utils'; | |||||||
| import { stripAnsiEscapes } from './util'; | import { stripAnsiEscapes } from './util'; | ||||||
| import { codeFrameColumns } from './transform/babelBundle'; | import { codeFrameColumns } from './transform/babelBundle'; | ||||||
| 
 | 
 | ||||||
| import type { TestInfo } from '../types/test'; |  | ||||||
| import type { MetadataWithCommitInfo } from './isomorphic/types'; | import type { MetadataWithCommitInfo } from './isomorphic/types'; | ||||||
| import type { TestInfoImpl } from './worker/testInfo'; | import type { TestInfoImpl } from './worker/testInfo'; | ||||||
| 
 | 
 | ||||||
| export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<string, string>, ariaSnapshot: string | undefined) { | export async function attachErrorContext(testInfo: TestInfoImpl, format: 'markdown' | 'json', sourceCache: Map<string, string>, ariaSnapshot: string | undefined) { | ||||||
|   if (process.env.PLAYWRIGHT_NO_COPY_PROMPT) |   if (format === 'json') { | ||||||
|  |     if (!ariaSnapshot) | ||||||
|       return; |       return; | ||||||
| 
 | 
 | ||||||
|  |     testInfo._attach({ | ||||||
|  |       name: `_error-context`, | ||||||
|  |       contentType: 'application/json', | ||||||
|  |       body: Buffer.from(JSON.stringify({ | ||||||
|  |         pageSnapshot: ariaSnapshot, | ||||||
|  |       })), | ||||||
|  |     }, undefined); | ||||||
|  | 
 | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const meaningfulSingleLineErrors = new Set(testInfo.errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!)); |   const meaningfulSingleLineErrors = new Set(testInfo.errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!)); | ||||||
|   for (const error of testInfo.errors) { |   for (const error of testInfo.errors) { | ||||||
|     for (const singleLineError of meaningfulSingleLineErrors.keys()) { |     for (const singleLineError of meaningfulSingleLineErrors.keys()) { | ||||||
| @ -51,16 +62,10 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st | |||||||
| 
 | 
 | ||||||
|   for (const [index, error] of errors) { |   for (const [index, error] of errors) { | ||||||
|     const metadata = testInfo.config.metadata as MetadataWithCommitInfo; |     const metadata = testInfo.config.metadata as MetadataWithCommitInfo; | ||||||
|     if (testInfo.attachments.find(a => a.name === `_prompt-${index}`)) |     if (testInfo.attachments.find(a => a.name === `_error-context-${index}`)) | ||||||
|       continue; |       continue; | ||||||
| 
 | 
 | ||||||
|     const promptParts = [ |     const lines = [ | ||||||
|       `# Instructions`, |  | ||||||
|       '', |  | ||||||
|       `- Following Playwright test failed.`, |  | ||||||
|       `- Explain why, be concise, respect Playwright best practices.`, |  | ||||||
|       `- Provide a snippet of code with the fix, if possible.`, |  | ||||||
|       '', |  | ||||||
|       `# Test info`, |       `# Test info`, | ||||||
|       '', |       '', | ||||||
|       `- Name: ${testInfo.titlePath.slice(1).join(' >> ')}`, |       `- Name: ${testInfo.titlePath.slice(1).join(' >> ')}`, | ||||||
| @ -74,7 +79,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st | |||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     if (ariaSnapshot) { |     if (ariaSnapshot) { | ||||||
|       promptParts.push( |       lines.push( | ||||||
|           '', |           '', | ||||||
|           '# Page snapshot', |           '# Page snapshot', | ||||||
|           '', |           '', | ||||||
| @ -103,7 +108,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st | |||||||
|           message: inlineMessage || undefined, |           message: inlineMessage || undefined, | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|     promptParts.push( |     lines.push( | ||||||
|         '', |         '', | ||||||
|         '# Test source', |         '# Test source', | ||||||
|         '', |         '', | ||||||
| @ -113,7 +118,7 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     if (metadata.gitDiff) { |     if (metadata.gitDiff) { | ||||||
|       promptParts.push( |       lines.push( | ||||||
|           '', |           '', | ||||||
|           '# Local changes', |           '# Local changes', | ||||||
|           '', |           '', | ||||||
| @ -123,30 +128,17 @@ export async function attachErrorPrompts(testInfo: TestInfo, sourceCache: Map<st | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const promptPath = testInfo.outputPath(errors.length === 1 ? `prompt.md` : `prompt-${index}.md`); |     const filePath = testInfo.outputPath(errors.length === 1 ? `error-context.md` : `error-context-${index}.md`); | ||||||
|     await fs.writeFile(promptPath, promptParts.join('\n'), 'utf8'); |     await fs.writeFile(filePath, lines.join('\n'), 'utf8'); | ||||||
| 
 | 
 | ||||||
|     (testInfo as TestInfoImpl)._attach({ |     (testInfo as TestInfoImpl)._attach({ | ||||||
|       name: `_prompt-${index}`, |       name: `_error-context-${index}`, | ||||||
|       contentType: 'text/markdown', |       contentType: 'text/markdown', | ||||||
|       path: promptPath, |       path: filePath, | ||||||
|     }, undefined); |     }, undefined); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function attachErrorContext(testInfo: TestInfo, ariaSnapshot: string | undefined) { |  | ||||||
|   if (!ariaSnapshot) |  | ||||||
|     return; |  | ||||||
| 
 |  | ||||||
|   (testInfo as TestInfoImpl)._attach({ |  | ||||||
|     name: `_error-context`, |  | ||||||
|     contentType: 'application/json', |  | ||||||
|     body: Buffer.from(JSON.stringify({ |  | ||||||
|       pageSnapshot: ariaSnapshot, |  | ||||||
|     })), |  | ||||||
|   }, undefined); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function loadSource(file: string, sourceCache: Map<string, string>) { | async function loadSource(file: string, sourceCache: Map<string, string>) { | ||||||
|   let source = sourceCache.get(file); |   let source = sourceCache.get(file); | ||||||
|   if (!source) { |   if (!source) { | ||||||
| @ -22,7 +22,7 @@ import { setBoxedStackPrefixes, asLocator, createGuid, currentZone, debugMode, i | |||||||
| 
 | 
 | ||||||
| import { currentTestInfo } from './common/globals'; | import { currentTestInfo } from './common/globals'; | ||||||
| import { rootTestType } from './common/testType'; | import { rootTestType } from './common/testType'; | ||||||
| import { attachErrorContext, attachErrorPrompts } from './prompt'; | import { attachErrorContext } from './errorContext'; | ||||||
| 
 | 
 | ||||||
| import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; | import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; | ||||||
| import type { ContextReuseMode } from './common/config'; | import type { ContextReuseMode } from './common/config'; | ||||||
| @ -55,13 +55,15 @@ type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { | |||||||
|   _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>; |   _contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | type ErrorContextOption = { format: 'json' | 'markdown' } | undefined; | ||||||
|  | 
 | ||||||
| type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { | type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { | ||||||
|   playwright: PlaywrightImpl; |   playwright: PlaywrightImpl; | ||||||
|   _browserOptions: LaunchOptions; |   _browserOptions: LaunchOptions; | ||||||
|   _optionContextReuseMode: ContextReuseMode, |   _optionContextReuseMode: ContextReuseMode, | ||||||
|   _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], |   _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], | ||||||
|   _reuseContext: boolean, |   _reuseContext: boolean, | ||||||
|   _optionAttachErrorContext: boolean, |   _optionErrorContext: ErrorContextOption, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ | const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ | ||||||
| @ -245,13 +247,13 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ | |||||||
|     playwright._defaultContextNavigationTimeout = undefined; |     playwright._defaultContextNavigationTimeout = undefined; | ||||||
|   }, { auto: 'all-hooks-included',  title: 'context configuration', box: true } as any], |   }, { auto: 'all-hooks-included',  title: 'context configuration', box: true } as any], | ||||||
| 
 | 
 | ||||||
|   _setupArtifacts: [async ({ playwright, screenshot, _optionAttachErrorContext }, use, testInfo) => { |   _setupArtifacts: [async ({ playwright, screenshot, _optionErrorContext }, use, testInfo) => { | ||||||
|     // This fixture has a separate zero-timeout slot to ensure that artifact collection
 |     // This fixture has a separate zero-timeout slot to ensure that artifact collection
 | ||||||
|     // happens even after some fixtures or hooks time out.
 |     // happens even after some fixtures or hooks time out.
 | ||||||
|     // Now that default test timeout is known, we can replace zero with an actual value.
 |     // Now that default test timeout is known, we can replace zero with an actual value.
 | ||||||
|     testInfo.setTimeout(testInfo.project.timeout); |     testInfo.setTimeout(testInfo.project.timeout); | ||||||
| 
 | 
 | ||||||
|     const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, _optionAttachErrorContext); |     const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot, _optionErrorContext); | ||||||
|     await artifactsRecorder.willStartTest(testInfo as TestInfoImpl); |     await artifactsRecorder.willStartTest(testInfo as TestInfoImpl); | ||||||
| 
 | 
 | ||||||
|     const tracingGroupSteps: TestStepInternal[] = []; |     const tracingGroupSteps: TestStepInternal[] = []; | ||||||
| @ -393,7 +395,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ | |||||||
| 
 | 
 | ||||||
|   _optionContextReuseMode: ['none', { scope: 'worker', option: true }], |   _optionContextReuseMode: ['none', { scope: 'worker', option: true }], | ||||||
|   _optionConnectOptions: [undefined, { scope: 'worker', option: true }], |   _optionConnectOptions: [undefined, { scope: 'worker', option: true }], | ||||||
|   _optionAttachErrorContext: [false, { scope: 'worker', option: true }], |   _optionErrorContext: [process.env.PLAYWRIGHT_NO_COPY_PROMPT ? undefined : { format: 'markdown' }, { scope: 'worker', option: true }], | ||||||
| 
 | 
 | ||||||
|   _reuseContext: [async ({ video, _optionContextReuseMode }, use) => { |   _reuseContext: [async ({ video, _optionContextReuseMode }, use) => { | ||||||
|     let mode = _optionContextReuseMode; |     let mode = _optionContextReuseMode; | ||||||
| @ -622,12 +624,12 @@ class ArtifactsRecorder { | |||||||
|   private _screenshotRecorder: SnapshotRecorder; |   private _screenshotRecorder: SnapshotRecorder; | ||||||
|   private _pageSnapshot: string | undefined; |   private _pageSnapshot: string | undefined; | ||||||
|   private _sourceCache: Map<string, string> = new Map(); |   private _sourceCache: Map<string, string> = new Map(); | ||||||
|   private _attachErrorContext: boolean; |   private _errorContext: ErrorContextOption; | ||||||
| 
 | 
 | ||||||
|   constructor(playwright: PlaywrightImpl, artifactsDir: string, screenshot: ScreenshotOption, attachErrorContext: boolean) { |   constructor(playwright: PlaywrightImpl, artifactsDir: string, screenshot: ScreenshotOption, errorContext: ErrorContextOption) { | ||||||
|     this._playwright = playwright; |     this._playwright = playwright; | ||||||
|     this._artifactsDir = artifactsDir; |     this._artifactsDir = artifactsDir; | ||||||
|     this._attachErrorContext = attachErrorContext; |     this._errorContext = errorContext; | ||||||
|     const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot; |     const screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot; | ||||||
|     this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts'); |     this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts'); | ||||||
| 
 | 
 | ||||||
| @ -671,7 +673,7 @@ class ArtifactsRecorder { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async _takePageSnapshot(context: BrowserContext) { |   private async _takePageSnapshot(context: BrowserContext) { | ||||||
|     if (process.env.PLAYWRIGHT_NO_COPY_PROMPT) |     if (!this._errorContext) | ||||||
|       return; |       return; | ||||||
|     if (this._testInfo.errors.length === 0) |     if (this._testInfo.errors.length === 0) | ||||||
|       return; |       return; | ||||||
| @ -719,10 +721,8 @@ class ArtifactsRecorder { | |||||||
|     if (context) |     if (context) | ||||||
|       await this._takePageSnapshot(context); |       await this._takePageSnapshot(context); | ||||||
| 
 | 
 | ||||||
|     if (this._attachErrorContext) |     if (this._errorContext) | ||||||
|       await attachErrorContext(this._testInfo, this._pageSnapshot); |       await attachErrorContext(this._testInfo, this._errorContext.format, this._sourceCache, this._pageSnapshot); | ||||||
|     else |  | ||||||
|       await attachErrorPrompts(this._testInfo, this._sourceCache, this._pageSnapshot); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async _startTraceChunkOnContextCreation(tracing: Tracing) { |   private async _startTraceChunkOnContextCreation(tracing: Tracing) { | ||||||
|  | |||||||
| @ -102,7 +102,7 @@ export interface TestServerInterface { | |||||||
|     projects?: string[]; |     projects?: string[]; | ||||||
|     reuseContext?: boolean; |     reuseContext?: boolean; | ||||||
|     connectWsEndpoint?: string; |     connectWsEndpoint?: string; | ||||||
|     attachErrorContext?: boolean; |     errorContext?: { format: 'json' | 'markdown' }; | ||||||
|   }): Promise<{ |   }): Promise<{ | ||||||
|     status: reporterTypes.FullResult['status']; |     status: reporterTypes.FullResult['status']; | ||||||
|   }>; |   }>; | ||||||
|  | |||||||
| @ -337,9 +337,9 @@ export function formatFailure(screen: Screen, config: FullConfig, test: TestCase | |||||||
|     // }
 |     // }
 | ||||||
|     for (let i = 0; i < result.attachments.length; ++i) { |     for (let i = 0; i < result.attachments.length; ++i) { | ||||||
|       const attachment = result.attachments[i]; |       const attachment = result.attachments[i]; | ||||||
|       if (attachment.name.startsWith('_prompt') && attachment.path) { |       if (attachment.name.startsWith('_error-context') && attachment.path) { | ||||||
|         resultLines.push(''); |         resultLines.push(''); | ||||||
|         resultLines.push(screen.colors.dim(`    Error Prompt: ${relativeFilePath(screen, config, attachment.path)}`)); |         resultLines.push(screen.colors.dim(`    Error Context: ${relativeFilePath(screen, config, attachment.path)}`)); | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|       if (attachment.name.startsWith('_')) |       if (attachment.name.startsWith('_')) | ||||||
|  | |||||||
| @ -314,7 +314,7 @@ export class TestServerDispatcher implements TestServerInterface { | |||||||
|         ...(params.headed !== undefined ? { headless: !params.headed } : {}), |         ...(params.headed !== undefined ? { headless: !params.headed } : {}), | ||||||
|         _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, |         _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, | ||||||
|         _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, |         _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, | ||||||
|         _optionAttachErrorContext: params.attachErrorContext, |         _optionErrorContext: params.errorContext, | ||||||
|       }, |       }, | ||||||
|       ...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}), |       ...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}), | ||||||
|       ...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}), |       ...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}), | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ import { ToolbarButton } from '@web/components/toolbarButton'; | |||||||
| import { useIsLLMAvailable, useLLMChat } from './llm'; | import { useIsLLMAvailable, useLLMChat } from './llm'; | ||||||
| import { useAsyncMemo } from '@web/uiUtils'; | import { useAsyncMemo } from '@web/uiUtils'; | ||||||
| import { attachmentURL } from './attachmentsTab'; | import { attachmentURL } from './attachmentsTab'; | ||||||
|  | import { fixTestInstructions } from '@web/prompts'; | ||||||
| 
 | 
 | ||||||
| const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => { | const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => { | ||||||
|   return ( |   return ( | ||||||
| @ -67,10 +68,10 @@ function Error({ message, error, errorId, sdkLanguage, revealInSource }: { messa | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const prompt = useAsyncMemo(async () => { |   const prompt = useAsyncMemo(async () => { | ||||||
|     if (!error.prompt) |     if (!error.context) | ||||||
|       return; |       return; | ||||||
|     const response = await fetch(attachmentURL(error.prompt)); |     const response = await fetch(attachmentURL(error.context)); | ||||||
|     return await response.text(); |     return fixTestInstructions + await response.text(); | ||||||
|   }, [error], undefined); |   }, [error], undefined); | ||||||
| 
 | 
 | ||||||
|   return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}> |   return <div style={{ display: 'flex', flexDirection: 'column', overflowX: 'clip' }}> | ||||||
|  | |||||||
| @ -56,7 +56,7 @@ export type ErrorDescription = { | |||||||
|   action?: ActionTraceEventInContext; |   action?: ActionTraceEventInContext; | ||||||
|   stack?: StackFrame[]; |   stack?: StackFrame[]; | ||||||
|   message: string; |   message: string; | ||||||
|   prompt?: trace.AfterActionTraceEventAttachment & { traceUrl: string }; |   context?: trace.AfterActionTraceEventAttachment & { traceUrl: string }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string }; | export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string }; | ||||||
| @ -141,7 +141,7 @@ export class MultiTraceModel { | |||||||
|     return this.errors.filter(e => !!e.message).map((error, i) => ({ |     return this.errors.filter(e => !!e.message).map((error, i) => ({ | ||||||
|       stack: error.stack, |       stack: error.stack, | ||||||
|       message: error.message, |       message: error.message, | ||||||
|       prompt: this.attachments.find(a => a.name === `_prompt-${i}`), |       context: this.attachments.find(a => a.name === `_error-context-${i}`), | ||||||
|     })); |     })); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								packages/web/src/prompts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/web/src/prompts.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | /** | ||||||
|  |  * Copyright (c) Microsoft Corporation. | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  * http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | export const fixTestInstructions = ` | ||||||
|  | # Instructions | ||||||
|  | 
 | ||||||
|  | - Following Playwright test failed. | ||||||
|  | - Explain why, be concise, respect Playwright best practices. | ||||||
|  | - Provide a snippet of code with the fix, if possible. | ||||||
|  | `.trimStart();
 | ||||||
| @ -167,7 +167,7 @@ test('should print debug log when failed to connect', async ({ runInlineTest }) | |||||||
|   expect(result.exitCode).toBe(1); |   expect(result.exitCode).toBe(1); | ||||||
|   expect(result.failed).toBe(1); |   expect(result.failed).toBe(1); | ||||||
|   expect(result.output).toContain('b-debug-log-string'); |   expect(result.output).toContain('b-debug-log-string'); | ||||||
|   expect(result.results[0].attachments).toEqual([expect.objectContaining({ name: '_prompt-0' })]); |   expect(result.results[0].attachments).toEqual([expect.objectContaining({ name: '_error-context-0' })]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should record trace', async ({ runInlineTest }) => { | test('should record trace', async ({ runInlineTest }) => { | ||||||
| @ -223,7 +223,7 @@ test('should record trace', async ({ runInlineTest }) => { | |||||||
|     'After Hooks', |     'After Hooks', | ||||||
|     'fixture: page', |     'fixture: page', | ||||||
|     'fixture: context', |     'fixture: context', | ||||||
|     '_attach "_prompt-0"', |     '_attach "_error-context-0"', | ||||||
|     'Worker Cleanup', |     'Worker Cleanup', | ||||||
|     'fixture: browser', |     'fixture: browser', | ||||||
|   ]); |   ]); | ||||||
|  | |||||||
| @ -510,13 +510,13 @@ test('should work with video: on-first-retry', async ({ runInlineTest }) => { | |||||||
|   expect(fs.existsSync(dirPass)).toBeFalsy(); |   expect(fs.existsSync(dirPass)).toBeFalsy(); | ||||||
| 
 | 
 | ||||||
|   const dirFail = test.info().outputPath('test-results', 'a-fail-chromium'); |   const dirFail = test.info().outputPath('test-results', 'a-fail-chromium'); | ||||||
|   expect(fs.readdirSync(dirFail)).toEqual(['prompt.md']); |   expect(fs.readdirSync(dirFail)).toEqual(['error-context.md']); | ||||||
| 
 | 
 | ||||||
|   const dirRetry = test.info().outputPath('test-results', 'a-fail-chromium-retry1'); |   const dirRetry = test.info().outputPath('test-results', 'a-fail-chromium-retry1'); | ||||||
|   const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm')); |   const videoFailRetry = fs.readdirSync(dirRetry).find(file => file.endsWith('webm')); | ||||||
|   expect(videoFailRetry).toBeTruthy(); |   expect(videoFailRetry).toBeTruthy(); | ||||||
| 
 | 
 | ||||||
|   const errorPrompt = expect.objectContaining({ name: '_prompt-0' }); |   const errorPrompt = expect.objectContaining({ name: '_error-context-0' }); | ||||||
|   expect(result.report.suites[0].specs[1].tests[0].results[0].attachments).toEqual([errorPrompt]); |   expect(result.report.suites[0].specs[1].tests[0].results[0].attachments).toEqual([errorPrompt]); | ||||||
|   expect(result.report.suites[0].specs[1].tests[0].results[1].attachments).toEqual([{ |   expect(result.report.suites[0].specs[1].tests[0].results[1].attachments).toEqual([{ | ||||||
|     name: 'video', |     name: 'video', | ||||||
|  | |||||||
| @ -359,7 +359,7 @@ test('should report parallelIndex', async ({ runInlineTest }, testInfo) => { | |||||||
| test('attaches error context', async ({ runInlineTest }) => { | test('attaches error context', async ({ runInlineTest }) => { | ||||||
|   const result = await runInlineTest({ |   const result = await runInlineTest({ | ||||||
|     'playwright.config.ts': ` |     'playwright.config.ts': ` | ||||||
|           export default { use: { _optionAttachErrorContext: true } }; |           export default { use: { _optionErrorContext: { format: 'json' } } }; | ||||||
|     `,
 |     `,
 | ||||||
|     'a.test.js': ` |     'a.test.js': ` | ||||||
|       const { test, expect } = require('@playwright/test'); |       const { test, expect } = require('@playwright/test'); | ||||||
|  | |||||||
| @ -190,7 +190,7 @@ for (const useIntermediateMergeReport of [false, true] as const) { | |||||||
|       expect(result.exitCode).toBe(1); |       expect(result.exitCode).toBe(1); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     test('should show error prompt with relative path', async ({ runInlineTest, useIntermediateMergeReport }) => { |     test('should show error context with relative path', async ({ runInlineTest, useIntermediateMergeReport }) => { | ||||||
|       const result = await runInlineTest({ |       const result = await runInlineTest({ | ||||||
|         'a.test.js': ` |         'a.test.js': ` | ||||||
|           const { test, expect } = require('@playwright/test'); |           const { test, expect } = require('@playwright/test'); | ||||||
| @ -201,9 +201,9 @@ for (const useIntermediateMergeReport of [false, true] as const) { | |||||||
|       }, { reporter: 'line' }); |       }, { reporter: 'line' }); | ||||||
|       const text = result.output; |       const text = result.output; | ||||||
|       if (useIntermediateMergeReport) |       if (useIntermediateMergeReport) | ||||||
|         expect(text).toContain(`Error Prompt: ${path.join('blob-report', 'resources')}`); |         expect(text).toContain(`Error Context: ${path.join('blob-report', 'resources')}`); | ||||||
|       else |       else | ||||||
|         expect(text).toContain(`Error Prompt: ${path.join('test-results', 'a-one', 'prompt.md')}`); |         expect(text).toContain(`Error Context: ${path.join('test-results', 'a-one', 'error-context.md')}`); | ||||||
|       expect(result.exitCode).toBe(1); |       expect(result.exitCode).toBe(1); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Simon Knott
						Simon Knott