mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: render error context to copy prompt (#36123)
This commit is contained in:
parent
c587492ca5
commit
43a086a8de
11
package-lock.json
generated
11
package-lock.json
generated
@ -1672,6 +1672,13 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__code-frame": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.6.tgz",
|
||||
"integrity": "sha512-Anitqkl3+KrzcW2k77lRlg/GfLZLWXBuNgbEcIOU6M92yw42vsd3xV/Z/yAHEj8m+KUjL6bWOVOFqX8PFPJ4LA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@ -8765,7 +8772,11 @@
|
||||
"packages/trace-viewer": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__code-frame": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"packages/web": {
|
||||
|
||||
@ -144,6 +144,7 @@ const TestCaseViewLoader: React.FC<{
|
||||
return <div className='test-case-column'>
|
||||
<TestCaseView
|
||||
projectNames={report.json().projectNames}
|
||||
testRunMetadata={report.json().metadata}
|
||||
next={next}
|
||||
prev={prev}
|
||||
test={test}
|
||||
|
||||
@ -66,7 +66,7 @@ const testCase: TestCase = {
|
||||
};
|
||||
|
||||
test('should render test case', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await expect(component.getByText('Hidden annotation')).toBeHidden();
|
||||
await component.getByText('Annotations').click();
|
||||
@ -82,7 +82,7 @@ test('should render test case', async ({ mount }) => {
|
||||
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await component.getByText('Annotation text', { exact: false }).first().hover();
|
||||
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
|
||||
@ -113,7 +113,7 @@ const annotationLinkRenderingTestCase: TestCase = {
|
||||
};
|
||||
|
||||
test('should correctly render links in annotations', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
|
||||
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
|
||||
await expect(firstLink).toBeVisible();
|
||||
@ -188,7 +188,7 @@ const testCaseSummary: TestCaseSummary = {
|
||||
|
||||
|
||||
test('should correctly render links in attachments', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await component.getByText('first attachment').click();
|
||||
const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
|
||||
await expect(body).toBeVisible();
|
||||
@ -201,7 +201,7 @@ test('should correctly render links in attachments', async ({ mount }) => {
|
||||
});
|
||||
|
||||
test('should correctly render links in attachment name', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const link = component.getByText('attachment with inline link').locator('a');
|
||||
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
|
||||
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
|
||||
@ -211,7 +211,7 @@ test('should correctly render links in attachment name', async ({ mount }) => {
|
||||
});
|
||||
|
||||
test('should correctly render prev and next', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0}></TestCaseView>);
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- text: group
|
||||
- link "« previous"
|
||||
@ -226,7 +226,7 @@ const testCaseWithTwoAttempts: TestCase = {
|
||||
results: [
|
||||
{
|
||||
...result,
|
||||
errors: ['Error message'],
|
||||
errors: [{ message: 'Error message' }],
|
||||
status: 'failed',
|
||||
duration: 50,
|
||||
},
|
||||
@ -239,7 +239,7 @@ const testCaseWithTwoAttempts: TestCase = {
|
||||
};
|
||||
|
||||
test('total duration is selected run duration', async ({ mount, page }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCaseWithTwoAttempts} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} testRunMetadata={{}} test={testCaseWithTwoAttempts} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- text: "My test test.spec.ts:42 200ms"
|
||||
- tablist:
|
||||
|
||||
@ -29,14 +29,16 @@ import { hashStringToInt, msToString } from './utils';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { CopyToClipboardContainer } from './copyToClipboard';
|
||||
import { HeaderView } from './headerView';
|
||||
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
|
||||
|
||||
export const TestCaseView: React.FC<{
|
||||
projectNames: string[],
|
||||
test: TestCase,
|
||||
testRunMetadata: MetadataWithCommitInfo | undefined,
|
||||
next: TestCaseSummary | undefined,
|
||||
prev: TestCaseSummary | undefined,
|
||||
run: number,
|
||||
}> = ({ projectNames, test, run, next, prev }) => {
|
||||
}> = ({ projectNames, test, testRunMetadata, run, next, prev }) => {
|
||||
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
|
||||
@ -83,7 +85,7 @@ export const TestCaseView: React.FC<{
|
||||
{!!visibleAnnotations.length && <AutoChip header='Annotations' dataTestId='test-case-annotations'>
|
||||
{visibleAnnotations.map((annotation, index) => <TestCaseAnnotationView key={index} annotation={annotation} />)}
|
||||
</AutoChip>}
|
||||
<TestResultView test={test!} result={result} />
|
||||
<TestResultView test={test!} result={result} testRunMetadata={testRunMetadata} />
|
||||
</>;
|
||||
},
|
||||
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />
|
||||
|
||||
@ -19,8 +19,6 @@ import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestAttachment } from './types';
|
||||
import { fixTestInstructions } from '@web/prompts';
|
||||
|
||||
export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<{ code: string; testId?: string; }>) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(code), [code]);
|
||||
@ -32,14 +30,12 @@ export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<
|
||||
);
|
||||
};
|
||||
|
||||
export const PromptButton: React.FC<{ context?: TestAttachment }> = ({ context }) => {
|
||||
export const PromptButton: React.FC<{ prompt: string }> = ({ prompt }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
return <button
|
||||
className='button'
|
||||
style={{ minWidth: 100 }}
|
||||
onClick={async () => {
|
||||
const contextText = context?.path ? await fetch(context.path!).then(r => r.text()) : context?.body;
|
||||
const prompt = fixTestInstructions + contextText; // TODO in next PR: enrich with test location, error details and source code.
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
|
||||
@ -27,6 +27,9 @@ import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { CodeSnippet, PromptButton, TestScreenshotErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import './testResultView.css';
|
||||
import { useAsyncMemo } from '@web/uiUtils';
|
||||
import { copyPrompt } from '@web/shared/prompts';
|
||||
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
|
||||
|
||||
interface ImageDiffWithAnchors extends ImageDiff {
|
||||
anchors: string[];
|
||||
@ -70,7 +73,8 @@ function groupImageDiffs(screenshots: Set<TestAttachment>, result: TestResult):
|
||||
export const TestResultView: React.FC<{
|
||||
test: TestCase,
|
||||
result: TestResult,
|
||||
}> = ({ test, result }) => {
|
||||
testRunMetadata: MetadataWithCommitInfo | undefined,
|
||||
}> = ({ test, result, testRunMetadata }) => {
|
||||
const { screenshots, videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext } = React.useMemo(() => {
|
||||
const attachments = result.attachments.filter(a => !a.name.startsWith('_'));
|
||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||
@ -82,15 +86,30 @@ export const TestResultView: React.FC<{
|
||||
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a));
|
||||
const otherAttachmentAnchors = [...otherAttachments].map(a => `attachment-${attachments.indexOf(a)}`);
|
||||
const diffs = groupImageDiffs(screenshots, result);
|
||||
const errors = classifyErrors(result.errors, diffs);
|
||||
const errors = classifyErrors(result.errors.map(e => e.message), diffs);
|
||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext };
|
||||
}, [result]);
|
||||
|
||||
const prompt = useAsyncMemo(async () => {
|
||||
return await copyPrompt({
|
||||
testInfo: [
|
||||
`- Name: ${test.path.join(' >> ')} >> ${test.title}`,
|
||||
`- Location: ${test.location.file}:${test.location.line}:${test.location.column}`
|
||||
].join('\n'),
|
||||
metadata: testRunMetadata,
|
||||
errorContext: errorContext?.path ? await fetch(errorContext.path!).then(r => r.text()) : errorContext?.body,
|
||||
errors: result.errors,
|
||||
buildCodeFrame: async error => error.codeframe,
|
||||
});
|
||||
}, [test, errorContext, testRunMetadata, result], undefined);
|
||||
|
||||
return <div className='test-result'>
|
||||
{!!errors.length && <AutoChip header='Errors'>
|
||||
<div style={{ position: 'absolute', right: '16px', padding: '10px', zIndex: 1 }}>
|
||||
<PromptButton context={errorContext} />
|
||||
</div>
|
||||
{prompt && (
|
||||
<div style={{ position: 'absolute', right: '16px', padding: '10px', zIndex: 1 }}>
|
||||
<PromptButton prompt={prompt} />
|
||||
</div>
|
||||
)}
|
||||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
|
||||
2
packages/html-reporter/src/types.d.ts
vendored
2
packages/html-reporter/src/types.d.ts
vendored
@ -94,7 +94,7 @@ export type TestResult = {
|
||||
startTime: string;
|
||||
duration: number;
|
||||
steps: TestStep[];
|
||||
errors: string[];
|
||||
errors: { message: string, codeframe?: string }[];
|
||||
attachments: TestAttachment[];
|
||||
status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
||||
annotations: TestAnnotation[];
|
||||
|
||||
@ -31,7 +31,7 @@ import { resolveReporterOutputPath, stripAnsiEscapes, stepTitle } from '../util'
|
||||
import type { ReporterV2 } from './reporterV2';
|
||||
import type { HtmlReporterOptions as HtmlReporterConfigOptions, Metadata, TestAnnotation } from '../../types/test';
|
||||
import type * as api from '../../types/testReporter';
|
||||
import type { HTMLReport, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
|
||||
import type { HTMLReport, Location, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
|
||||
import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
||||
import type { TransformCallback } from 'stream';
|
||||
import type { TestStepCategory } from '../util';
|
||||
@ -511,7 +511,12 @@ class HtmlBuilder {
|
||||
startTime: result.startTime.toISOString(),
|
||||
retry: result.retry,
|
||||
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s, result)),
|
||||
errors: formatResultFailure(internalScreen, test, result, '').map(error => error.message),
|
||||
errors: formatResultFailure(internalScreen, test, result, '').map(error => {
|
||||
return {
|
||||
message: error.message,
|
||||
codeframe: error.location ? createErrorCodeframe(error.message, error.location) : undefined
|
||||
};
|
||||
}),
|
||||
status: result.status,
|
||||
annotations: this._serializeAnnotations(result.annotations),
|
||||
attachments: this._serializeAttachments([
|
||||
@ -675,4 +680,29 @@ function createSnippets(stepsInFile: MultiMap<string, TestStep>) {
|
||||
}
|
||||
}
|
||||
|
||||
function createErrorCodeframe(message: string, location: Location) {
|
||||
let source: string;
|
||||
try {
|
||||
source = fs.readFileSync(location.file, 'utf-8') + '\n//';
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
return codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
highlightCode: false,
|
||||
linesAbove: 100,
|
||||
linesBelow: 100,
|
||||
message: stripAnsiEscapes(message).split('\n')[0] || undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default HtmlReporter;
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__code-frame": "^7.0.6"
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { codeFrameColumns } from '@babel/code-frame';
|
||||
import { ErrorMessage } from '@web/components/errorMessage';
|
||||
import * as React from 'react';
|
||||
import type * as modelUtil from './modelUtil';
|
||||
@ -23,7 +24,9 @@ import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||
import { useAsyncMemo } from '@web/uiUtils';
|
||||
import { attachmentURL } from './attachmentsTab';
|
||||
import { fixTestInstructions } from '@web/prompts';
|
||||
import { copyPrompt, stripAnsiEscapes } from '@web/shared/prompts';
|
||||
import { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
import { calculateSha1 } from './sourceTab';
|
||||
|
||||
const CopyPromptButton: React.FC<{ prompt: string }> = ({ prompt }) => {
|
||||
return (
|
||||
@ -86,7 +89,8 @@ export const ErrorsTab: React.FunctionComponent<{
|
||||
wallTime: number,
|
||||
sdkLanguage: Language,
|
||||
revealInSource: (error: modelUtil.ErrorDescription) => void,
|
||||
}> = ({ errorsModel, model, sdkLanguage, revealInSource, wallTime }) => {
|
||||
testRunMetadata: MetadataWithCommitInfo | undefined,
|
||||
}> = ({ errorsModel, model, sdkLanguage, revealInSource, wallTime, testRunMetadata }) => {
|
||||
const errorContext = useAsyncMemo(async () => {
|
||||
const attachment = model?.attachments.find(a => a.name === 'error-context');
|
||||
if (!attachment)
|
||||
@ -94,7 +98,49 @@ export const ErrorsTab: React.FunctionComponent<{
|
||||
return await fetch(attachmentURL(attachment)).then(r => r.text());
|
||||
}, [model], undefined);
|
||||
|
||||
const prompt = fixTestInstructions + (errorContext ?? ''); // TODO in next PR: enrich with test location, error details and source code, similar to errorContext.ts
|
||||
const buildCodeFrame = React.useCallback(async (error: modelUtil.ErrorDescription) => {
|
||||
const location = error.stack?.[0];
|
||||
if (!location)
|
||||
return;
|
||||
|
||||
let response = await fetch(`sha1/src@${await calculateSha1(location.file)}.txt`);
|
||||
if (response.status === 404)
|
||||
response = await fetch(`file?path=${encodeURIComponent(location.file)}`);
|
||||
if (response.status >= 400)
|
||||
return;
|
||||
|
||||
const source = await response.text();
|
||||
|
||||
return codeFrameColumns(
|
||||
source,
|
||||
{
|
||||
start: {
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
highlightCode: false,
|
||||
linesAbove: 100,
|
||||
linesBelow: 100,
|
||||
message: stripAnsiEscapes(error.message).split('\n')[0] || undefined,
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
const prompt = useAsyncMemo(
|
||||
() => copyPrompt(
|
||||
{
|
||||
testInfo: model?.title ?? '',
|
||||
metadata: testRunMetadata,
|
||||
errorContext,
|
||||
errors: model?.errorDescriptors ?? [],
|
||||
buildCodeFrame
|
||||
}
|
||||
),
|
||||
[errorContext, testRunMetadata, model, buildCodeFrame],
|
||||
undefined
|
||||
);
|
||||
|
||||
if (!errorsModel.errors.size)
|
||||
return <PlaceholderPanel text='No errors' />;
|
||||
|
||||
@ -43,6 +43,7 @@ import type { UITestStatus } from './testUtils';
|
||||
import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||
import type { HighlightedElement } from './snapshotTab';
|
||||
import type { TestAnnotation } from '@playwright/test';
|
||||
import { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
model?: modelUtil.MultiTraceModel,
|
||||
@ -56,7 +57,8 @@ export const Workbench: React.FunctionComponent<{
|
||||
inert?: boolean,
|
||||
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||
revealSource?: boolean,
|
||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
|
||||
testRunMetadata?: MetadataWithCommitInfo,
|
||||
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource, testRunMetadata }) => {
|
||||
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
|
||||
const [revealedError, setRevealedError] = React.useState<modelUtil.ErrorDescription | undefined>(undefined);
|
||||
const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
|
||||
@ -190,7 +192,7 @@ export const Workbench: React.FunctionComponent<{
|
||||
id: 'errors',
|
||||
title: 'Errors',
|
||||
errorCount: errorsModel.errors.size,
|
||||
render: () => <ErrorsTab errorsModel={errorsModel} model={model} sdkLanguage={sdkLanguage} revealInSource={error => {
|
||||
render: () => <ErrorsTab errorsModel={errorsModel} model={model} testRunMetadata={testRunMetadata} sdkLanguage={sdkLanguage} revealInSource={error => {
|
||||
if (error.action)
|
||||
setSelectedAction(error.action);
|
||||
else
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
114
packages/web/src/shared/prompts.ts
Normal file
114
packages/web/src/shared/prompts.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||
|
||||
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();
|
||||
|
||||
export async function copyPrompt<ErrorInfo extends { message: string }>({
|
||||
testInfo,
|
||||
metadata,
|
||||
errorContext,
|
||||
|
||||
errors,
|
||||
buildCodeFrame,
|
||||
}: {
|
||||
testInfo: string;
|
||||
metadata: MetadataWithCommitInfo | undefined;
|
||||
errorContext: string | undefined;
|
||||
|
||||
errors: ErrorInfo[];
|
||||
buildCodeFrame(error: ErrorInfo): Promise<string | undefined>;
|
||||
}) {
|
||||
const meaningfulSingleLineErrors = new Set(errors.filter(e => e.message && !e.message.includes('\n')).map(e => e.message!));
|
||||
for (const error of errors) {
|
||||
for (const singleLineError of meaningfulSingleLineErrors.keys()) {
|
||||
if (error.message?.includes(singleLineError))
|
||||
meaningfulSingleLineErrors.delete(singleLineError);
|
||||
}
|
||||
}
|
||||
|
||||
const meaningfulErrors = errors.filter(error => {
|
||||
if (!error.message)
|
||||
return false;
|
||||
|
||||
// Skip errors that are just a single line - they are likely to already be the error message.
|
||||
if (!error.message.includes('\n') && !meaningfulSingleLineErrors.has(error.message))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!meaningfulErrors.length)
|
||||
return undefined;
|
||||
|
||||
const lines = [
|
||||
fixTestInstructions,
|
||||
`# Test info`,
|
||||
'',
|
||||
testInfo,
|
||||
'',
|
||||
'# Error details',
|
||||
];
|
||||
|
||||
for (const error of meaningfulErrors) {
|
||||
lines.push(
|
||||
'',
|
||||
'```',
|
||||
stripAnsiEscapes(error.message || ''),
|
||||
'```',
|
||||
);
|
||||
}
|
||||
|
||||
if (errorContext)
|
||||
lines.push(errorContext);
|
||||
|
||||
const codeFrame = await buildCodeFrame(meaningfulErrors[meaningfulErrors.length - 1]);
|
||||
if (codeFrame) {
|
||||
lines.push(
|
||||
'',
|
||||
'# Test source',
|
||||
'',
|
||||
'```ts',
|
||||
codeFrame,
|
||||
'```',
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata?.gitDiff) {
|
||||
lines.push(
|
||||
'',
|
||||
'# Local changes',
|
||||
'',
|
||||
'```diff',
|
||||
metadata.gitDiff,
|
||||
'```',
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
|
||||
export function stripAnsiEscapes(str: string): string {
|
||||
return str.replace(ansiRegex, '');
|
||||
}
|
||||
@ -2874,7 +2874,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||
await expect(page.locator('.tree-item', { hasText: 'stdout' })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.fixme('should include diff in AI prompt', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
||||
test('should include diff in AI prompt', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
||||
const files = {
|
||||
'uncommitted.txt': `uncommitted file`,
|
||||
'playwright.config.ts': `export default {}`,
|
||||
|
||||
@ -555,7 +555,7 @@ test('skipped steps should have an indicator', async ({ runUITest }) => {
|
||||
await expect(skippedMarker).toHaveAccessibleName('skipped');
|
||||
});
|
||||
|
||||
test.fixme('should show copy prompt button in errors tab', async ({ runUITest }) => {
|
||||
test('should show copy prompt button in errors tab', async ({ runUITest }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user