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}`),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue