chore: another iteration on gitCommit/gitDiff props (#34926)

This commit is contained in:
Pavel Feldman 2025-02-26 08:40:30 -08:00 committed by GitHub
parent 17c4d8e5ec
commit cd23a224f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 261 additions and 205 deletions

View File

@ -239,7 +239,10 @@ export default defineConfig({
Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json.
Providing `'git.commit.info': {}` property will populate it with the git commit details. This is useful for CI/CD environments.
* Providing `gitCommit: 'generate'` property will populate it with the git commit details.
* Providing `gitDiff: 'generate'` property will populate it with the git diff details.
On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic generation.
**Usage**

View File

@ -20,32 +20,10 @@ import './common.css';
import './theme.css';
import './metadataView.css';
import type { Metadata } from '@playwright/test';
import type { GitCommitInfo } from '@testIsomorphic/types';
import type { CIInfo, GitCommitInfo, MetadataWithCommitInfo } from '@testIsomorphic/types';
import { CopyToClipboardContainer } from './copyToClipboard';
import { linkifyText } from '@web/renderUtils';
type MetadataEntries = [string, unknown][];
export const MetadataContext = React.createContext<MetadataEntries>([]);
export function MetadataProvider({ metadata, children }: React.PropsWithChildren<{ metadata: Metadata }>) {
const entries = React.useMemo(() => {
// TODO: do not plumb actualWorkers through metadata.
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
}, [metadata]);
return <MetadataContext.Provider value={entries}>{children}</MetadataContext.Provider>;
}
export function useMetadata() {
return React.useContext(MetadataContext);
}
export function useGitCommitInfo() {
const metadataEntries = useMetadata();
return metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
}
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
error: null,
@ -72,23 +50,22 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
}
}
export const MetadataView = () => {
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
export const MetadataView: React.FC<{ metadata: Metadata }> = params => {
return <ErrorBoundary><InnerMetadataView metadata={params.metadata}/></ErrorBoundary>;
};
const InnerMetadataView = () => {
const metadataEntries = useMetadata();
const gitCommitInfo = useGitCommitInfo();
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
if (!gitCommitInfo && !entries.length)
return null;
const InnerMetadataView: React.FC<{ metadata: Metadata }> = params => {
const commitInfo = params.metadata as MetadataWithCommitInfo;
const otherEntries = Object.entries(params.metadata).filter(([key]) => !ignoreKeys.has(key));
const hasMetadata = commitInfo.ci || commitInfo.gitCommit || otherEntries.length > 0;
if (!hasMetadata)
return;
return <div className='metadata-view'>
{gitCommitInfo && <>
<GitCommitInfoView info={gitCommitInfo}/>
{entries.length > 0 && <div className='metadata-separator' />}
</>}
{commitInfo.ci && <CiInfoView info={commitInfo.ci}/>}
{commitInfo.gitCommit && <GitCommitInfoView link={commitInfo.ci?.commitHref} info={commitInfo.gitCommit}/>}
{otherEntries.length > 0 && (commitInfo.gitCommit || commitInfo.ci) && <div className='metadata-separator' />}
<div className='metadata-section metadata-properties' role='list'>
{entries.map(([propertyName, value]) => {
{otherEntries.map(([propertyName, value]) => {
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
return (
@ -104,20 +81,24 @@ const InnerMetadataView = () => {
</div>;
};
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
const email = info.revision?.email ? ` <${info.revision?.email}>` : '';
const author = `${info.revision?.author || ''}${email}`;
const CiInfoView: React.FC<{ info: CIInfo }> = ({ info }) => {
const link = info.commitHref;
return <div className='metadata-section' role='list'>
<div role='listitem'>
<a href={link} target='_blank' rel='noopener noreferrer' title={link}>
{link}
</a>
</div>
</div>;
};
let subject = info.revision?.subject || '';
let link = info.revision?.link;
const GitCommitInfoView: React.FC<{ link?: string, info: GitCommitInfo }> = ({ link, info }) => {
const subject = info.subject;
const email = ` <${info.author.email}>`;
const author = `${info.author.name}${email}`;
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info.committer.time);
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info.committer.time);
if (info.pull_request?.link && info.pull_request?.title) {
subject = info.pull_request?.title;
link = info.pull_request?.link;
}
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info.revision?.timestamp);
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info.revision?.timestamp);
return <div className='metadata-section' role='list'>
<div role='listitem'>
{link ? (
@ -131,12 +112,13 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
<div role='listitem' className='hbox'>
<span className='mr-1'>{author}</span>
<span title={longTimestamp}> on {shortTimestamp}</span>
{info.ci?.link && (
<>
<span className='mx-2'>·</span>
<a href={info.ci?.link} target='_blank' rel='noopener noreferrer' title='CI/CD logs'>Logs</a>
</>
)}
</div>
</div>;
};
const ignoreKeys = new Set(['ci', 'gitCommit', 'gitDiff', 'actualWorkers']);
export const isMetadataEmpty = (metadata: MetadataWithCommitInfo): boolean => {
const otherEntries = Object.entries(metadata).filter(([key]) => !ignoreKeys.has(key));
return !metadata.ci && !metadata.gitCommit && !otherEntries.length;
};

View File

@ -0,0 +1,29 @@
/*
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 * as React from 'react';
import type { HTMLReport } from './types';
const HTMLReportContext = React.createContext<HTMLReport | undefined>(undefined);
export function HTMLReportContextProvider({ report, children }: React.PropsWithChildren<{ report: HTMLReport | undefined }>) {
return <HTMLReportContext.Provider value={report}>{children}</HTMLReportContext.Provider>;
}
export function useHTMLReport() {
return React.useContext(HTMLReportContext);
}

View File

@ -26,7 +26,7 @@ import './reportView.css';
import { TestCaseView } from './testCaseView';
import { TestFilesHeader, TestFilesView } from './testFilesView';
import './theme.css';
import { MetadataProvider } from './metadataView';
import { HTMLReportContextProvider } from './reportContext';
declare global {
interface Window {
@ -73,7 +73,7 @@ export const ReportView: React.FC<{
return result;
}, [report, filter]);
return <MetadataProvider metadata={report?.json().metadata ?? {}}><div className='htmlreport vbox px-4 pb-4'>
return <HTMLReportContextProvider report={report?.json()}><div className='htmlreport vbox px-4 pb-4'>
<main>
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
<Route predicate={testFilesRoutePredicate}>
@ -89,7 +89,7 @@ export const ReportView: React.FC<{
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
</Route>
</main>
</div></MetadataProvider>;
</div></HTMLReportContextProvider>;
};
const TestCaseViewLoader: React.FC<{

View File

@ -21,9 +21,14 @@ import type { ImageDiff } from '@web/shared/imageDiffView';
import { ImageDiffView } from '@web/shared/imageDiffView';
import type { TestResult } from './types';
import { fixTestPrompt } from '@web/components/prompts';
import { useGitCommitInfo } from './metadataView';
import { useHTMLReport } from './reportContext';
import type { MetadataWithCommitInfo } from '@playwright/isomorphic/types';
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
export const TestErrorView: React.FC<{
error: string;
testId?: string;
result?: TestResult
}> = ({ error, testId, result }) => {
return (
<CodeSnippet code={error} testId={testId}>
<div style={{ float: 'right', margin: 10 }}>
@ -47,12 +52,13 @@ const PromptButton: React.FC<{
error: string;
result?: TestResult;
}> = ({ error, result }) => {
const gitCommitInfo = useGitCommitInfo();
const report = useHTMLReport();
const commitInfo = report?.metadata as MetadataWithCommitInfo | undefined;
const prompt = React.useMemo(() => fixTestPrompt(
error,
gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff,
commitInfo?.gitDiff,
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
), [gitCommitInfo, result, error]);
), [commitInfo, result, error]);
const [copied, setCopied] = React.useState(false);

View File

@ -22,7 +22,7 @@ import { msToString } from './utils';
import { AutoChip } from './chip';
import { TestErrorView } from './testErrorView';
import * as icons from './icons';
import { MetadataView, useMetadata } from './metadataView';
import { isMetadataEmpty, MetadataView } from './metadataView';
export const TestFilesView: React.FC<{
tests: TestFileSummary[],
@ -67,13 +67,12 @@ export const TestFilesHeader: React.FC<{
metadataVisible: boolean,
toggleMetadataVisible: () => void,
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
const metadataEntries = useMetadata();
if (!report)
return null;
return <>
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
<div className='test-file-header-info'>
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
{!isMetadataEmpty(report.metadata) && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
</div>}
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</div>}
@ -83,7 +82,7 @@ export const TestFilesHeader: React.FC<{
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
</div>
{metadataVisible && <MetadataView/>}
{metadataVisible && <MetadataView metadata={report.metadata}/>}
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
</AutoChip>}

View File

@ -14,23 +14,40 @@
* limitations under the License.
*/
export interface GitCommitInfo {
revision?: {
id?: string;
author?: string;
email?: string;
subject?: string;
timestamp?: number;
link?: string;
diff?: string;
},
pull_request?: {
link?: string;
diff?: string;
base?: string;
title?: string;
},
ci?: {
link?: string;
}
}
export type GitCommitInfo = {
shortHash: string;
hash: string;
subject: string;
body: string;
author: {
name: string;
email: string;
time: number;
};
committer: {
name: string;
email: string
time: number;
};
branch: string;
};
export type CIInfo = {
commitHref: string;
buildHref?: string;
commitHash?: string;
baseHash?: string;
branch?: string;
};
export type UserMetadataWithCommitInfo = {
ci?: CIInfo;
gitCommit?: GitCommitInfo | 'generate';
gitDiff?: string | 'generate';
};
export type MetadataWithCommitInfo = {
ci?: CIInfo;
gitCommit?: GitCommitInfo;
gitDiff?: string;
};

View File

@ -14,119 +14,139 @@
* limitations under the License.
*/
import fs from 'fs';
import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
import { spawnAsync } from 'playwright-core/lib/utils';
import type { TestRunnerPlugin } from './';
import type { FullConfig } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config';
import type { GitCommitInfo } from '../isomorphic/types';
import type { GitCommitInfo, CIInfo, UserMetadataWithCommitInfo } from '../isomorphic/types';
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
const GIT_OPERATIONS_TIMEOUT_MS = 3000;
export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
const commitProperty = fullConfig.config.metadata['git.commit.info'];
if (commitProperty && typeof commitProperty === 'object' && Object.keys(commitProperty).length === 0)
fullConfig.plugins.push({ factory: gitCommitInfo });
fullConfig.plugins.push({ factory: gitCommitInfoPlugin });
};
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
type GitCommitInfoPluginOptions = {
directory?: string;
};
export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
return {
name: 'playwright:git-commit-info',
setup: async (config: FullConfig, configDir: string) => {
const commitInfo = await linksFromEnv();
await enrichStatusFromCLI(options?.directory || configDir, commitInfo);
config.metadata = config.metadata || {};
config.metadata['git.commit.info'] = commitInfo;
const metadata = config.metadata as UserMetadataWithCommitInfo;
const ci = ciInfo();
if (!metadata.ci && ci)
metadata.ci = ci;
if ((ci && !metadata.gitCommit) || metadata.gitCommit === 'generate') {
const git = await gitCommitInfo(options?.directory || configDir).catch(e => {
// eslint-disable-next-line no-console
console.error('Failed to get git commit info', e);
});
if (git)
metadata.gitCommit = git;
}
if ((ci && !metadata.gitDiff) || metadata.gitDiff === 'generate') {
const diffResult = await gitDiff(options?.directory || configDir, ci).catch(e => {
// eslint-disable-next-line no-console
console.error('Failed to get git diff', e);
});
if (diffResult)
metadata.gitDiff = diffResult;
}
},
};
};
interface GitCommitInfoPluginOptions {
directory?: string;
function ciInfo(): CIInfo | undefined {
if (process.env.GITHUB_ACTIONS) {
return {
commitHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`,
buildHref: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
commitHash: process.env.GITHUB_SHA,
baseHash: process.env.GITHUB_BASE_REF,
branch: process.env.GITHUB_REF_NAME,
};
}
if (process.env.GITLAB_CI) {
return {
commitHref: `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`,
buildHref: process.env.CI_JOB_URL,
commitHash: process.env.CI_COMMIT_SHA,
baseHash: process.env.CI_COMMIT_BEFORE_SHA,
branch: process.env.CI_COMMIT_REF_NAME,
};
}
if (process.env.JENKINS_URL && process.env.BUILD_URL) {
return {
commitHref: process.env.BUILD_URL,
commitHash: process.env.GIT_COMMIT,
baseHash: process.env.GIT_PREVIOUS_COMMIT,
branch: process.env.GIT_BRANCH,
};
}
// Open to PRs.
}
async function linksFromEnv(): Promise<GitCommitInfo> {
const out: GitCommitInfo = {};
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
if (process.env.BUILD_URL) {
out.ci = out.ci || {};
out.ci.link = process.env.BUILD_URL;
}
// GitLab: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
if (process.env.CI_PROJECT_URL && process.env.CI_COMMIT_SHA) {
out.revision = out.revision || {};
out.revision.link = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
}
if (process.env.CI_JOB_URL) {
out.ci = out.ci || {};
out.ci.link = process.env.CI_JOB_URL;
}
// GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_SHA) {
out.revision = out.revision || {};
out.revision.link = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`;
}
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID) {
out.ci = out.ci || {};
out.ci.link = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
}
if (process.env.GITHUB_EVENT_PATH) {
try {
const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf8'));
if (json.pull_request) {
out.pull_request = out.pull_request || {};
out.pull_request.title = json.pull_request.title;
out.pull_request.link = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${json.pull_request.number}`;
out.pull_request.base = json.pull_request.base.ref;
}
} catch {
}
}
return out;
}
async function enrichStatusFromCLI(gitDir: string, commitInfo: GitCommitInfo) {
const separator = `:${createGuid().slice(0, 4)}:`;
async function gitCommitInfo(gitDir: string): Promise<GitCommitInfo | undefined> {
const separator = `---786eec917292---`;
const tokens = [
'%H', // commit hash
'%h', // abbreviated commit hash
'%s', // subject
'%B', // raw body (unwrapped subject and body)
'%an', // author name
'%ae', // author email
'%at', // author date, UNIX timestamp
'%cn', // committer name
'%ce', // committer email
'%ct', // committer date, UNIX timestamp
'', // branch
];
const commitInfoResult = await spawnAsync(
'git',
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
`git log -1 --pretty=format:"${tokens.join(separator)}" && git rev-parse --abbrev-ref HEAD`, [],
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
);
if (commitInfoResult.code)
return;
return undefined;
const showOutput = commitInfoResult.stdout.trim();
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
let timestamp: number = Number.parseInt(rawTimestamp, 10);
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = showOutput.split(separator);
commitInfo.revision = {
...commitInfo.revision,
id,
author,
email,
return {
shortHash,
hash,
subject,
timestamp,
body,
author: {
name: authorName,
email: authorEmail,
time: +authorTime * 1000,
},
committer: {
name: committerName,
email: committerEmail,
time: +committerTime * 1000,
},
branch: branch.trim(),
};
const diffLimit = 1_000_000; // 1MB
if (commitInfo.pull_request?.base) {
const pullDiffResult = await spawnAsync(
'git',
['diff', commitInfo.pull_request?.base],
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
);
if (!pullDiffResult.code)
commitInfo.pull_request!.diff = pullDiffResult.stdout.substring(0, diffLimit);
} else {
const diffResult = await spawnAsync(
'git',
['diff', 'HEAD~1'],
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
);
if (!diffResult.code)
commitInfo.revision!.diff = diffResult.stdout.substring(0, diffLimit);
}
}
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
const diffLimit = 100_000;
const baseHash = ci?.baseHash ?? 'HEAD~1';
const pullDiffResult = await spawnAsync(
'git',
['diff', baseHash],
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
);
if (!pullDiffResult.code)
return pullDiffResult.stdout.substring(0, diffLimit);
}

View File

@ -35,4 +35,3 @@ export type TestRunnerPluginRegistration = {
};
export { webServer } from './webServerPlugin';
export { gitCommitInfo } from './gitCommitInfoPlugin';

View File

@ -1284,9 +1284,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
* key-value pairs, and JSON report will include metadata serialized as json.
* - Providing `gitCommit: 'generate'` property will populate it with the git commit details.
* - Providing `gitDiff: 'generate'` property will populate it with the git diff details.
*
* Providing `'git.commit.info': {}` property will populate it with the git commit details. This is useful for CI/CD
* environments.
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
* generation.
*
* **Usage**
*

View File

@ -24,20 +24,20 @@ 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';
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
import { AIConversation } from './aiConversation';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useIsLLMAvailable, useLLMChat } from './llm';
import { useAsyncMemo } from '@web/uiUtils';
const GitCommitInfoContext = React.createContext<GitCommitInfo | undefined>(undefined);
const CommitInfoContext = React.createContext<MetadataWithCommitInfo | undefined>(undefined);
export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) {
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
export function CommitInfoProvider({ children, commitInfo }: React.PropsWithChildren<{ commitInfo: MetadataWithCommitInfo }>) {
return <CommitInfoContext.Provider value={commitInfo}>{children}</CommitInfoContext.Provider>;
}
export function useGitCommitInfo() {
return React.useContext(GitCommitInfoContext);
export function useCommitInfo() {
return React.useContext(CommitInfoContext);
}
function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
@ -100,8 +100,7 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSource }: { message: string, error: ErrorDescription, errorId: string, sdkLanguage: Language, pageSnapshot?: string, revealInSource: (error: ErrorDescription) => void }) {
const [showLLM, setShowLLM] = React.useState(false);
const llmAvailable = useIsLLMAvailable();
const gitCommitInfo = useGitCommitInfo();
const diff = gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff;
const metadata = useCommitInfo();
let location: string | undefined;
let longLocation: string | undefined;
@ -127,8 +126,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
</div>}
<span style={{ position: 'absolute', right: '5px' }}>
{llmAvailable
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={diff} pageSnapshot={pageSnapshot} />
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />}
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={metadata?.gitDiff} pageSnapshot={pageSnapshot} />
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={metadata?.gitDiff} />}
</span>
</div>

View File

@ -37,8 +37,9 @@ import { TestListView } from './uiModeTestListView';
import { TraceView } from './uiModeTraceView';
import { SettingsView } from './settingsView';
import { DefaultSettingsView } from './defaultSettingsView';
import { GitCommitInfoProvider } from './errorsTab';
import { CommitInfoProvider } from './errorsTab';
import { LLMProvider } from './llm';
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
let xtermSize = { cols: 80, rows: 24 };
const xtermDataSource: XtermDataSource = {
@ -432,7 +433,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']}>
<CommitInfoProvider commitInfo={testModel?.config.metadata as MetadataWithCommitInfo}>
<TraceView
pathSeparator={queryParams.pathSeparator}
item={selectedItem}
@ -440,7 +441,7 @@ export const UIModeView: React.FC<{}> = ({
revealSource={revealSource}
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
/>
</GitCommitInfoProvider>
</CommitInfoProvider>
</div>
</div>}
sidebar={<div className='vbox ui-mode-sidebar'>

View File

@ -1187,12 +1187,12 @@ for (const useIntermediateMergeReport of [true, false] as const) {
]);
});
test('should include metadata with git.commit.info', async ({ runInlineTest, writeFiles, showReport, page }) => {
test('should include metadata with gitCommit', async ({ runInlineTest, writeFiles, showReport, page }) => {
const files = {
'uncommitted.txt': `uncommitted file`,
'playwright.config.ts': `
export default {
metadata: { 'git.commit.info': {}, foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
};
`,
'example.spec.ts': `
@ -1219,6 +1219,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
PLAYWRIGHT_HTML_OPEN: 'never',
GITHUB_ACTIONS: '1',
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
GITHUB_SERVER_URL: 'https://playwright.dev',
GITHUB_SHA: 'example-sha',
@ -1240,12 +1241,12 @@ for (const useIntermediateMergeReport of [true, false] as const) {
`);
});
test('should include metadata with git.commit.info on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => {
test('should include metadata on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => {
const files = {
'uncommitted.txt': `uncommitted file`,
'playwright.config.ts': `
export default {
metadata: { 'git.commit.info': {}, foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
};
`,
'example.spec.ts': `
@ -1281,6 +1282,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
PLAYWRIGHT_HTML_OPEN: 'never',
GITHUB_ACTIONS: '1',
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
GITHUB_RUN_ID: 'example-run-id',
GITHUB_SERVER_URL: 'https://playwright.dev',
@ -1295,10 +1297,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
- list:
- listitem:
- link "My PR"
- listitem:
- text: /William <shakespeare@example\\.local>/
- link "Logs"
- link "https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha"
- list:
- listitem: "foo : value1"
- listitem: "bar : {\\"prop\\":\\"value2\\"}"
@ -1306,7 +1305,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
`);
});
test('should not include git metadata w/o git.commit.info', async ({ runInlineTest, showReport, page }) => {
test('should not include git metadata w/o gitCommit', async ({ runInlineTest, showReport, page }) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {};
@ -1330,7 +1329,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
'playwright.config.ts': `
export default {
metadata: {
'git.commit.info': { revision: { timestamp: 'hi' } }
gitCommit: { author: { date: 'hi' } }
},
};
`,
@ -2765,7 +2764,6 @@ for (const useIntermediateMergeReport of [true, false] as const) {
'playwright.config.ts': `
export default {
metadata: {
'git.commit.info': {},
foo: 'value1',
bar: { prop: 'value2' },
baz: ['value3', 123]
@ -2799,6 +2797,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
PLAYWRIGHT_HTML_OPEN: 'never',
GITHUB_ACTIONS: '1',
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
GITHUB_RUN_ID: 'example-run-id',
GITHUB_SERVER_URL: 'https://playwright.dev',

View File

@ -21,14 +21,13 @@ test('should render html report git info metadata', async ({ runUITest }) => {
'reporter.ts': `
module.exports = class Reporter {
onBegin(config, suite) {
console.log('ci.link:', config.metadata['git.commit.info'].ci.link);
console.log('ci.link:', config.metadata['ci'].commitHref);
}
}
`,
'playwright.config.ts': `
import { defineConfig } from '@playwright/test';
export default defineConfig({
metadata: { 'git.commit.info': {} },
reporter: './reporter.ts',
});
`,
@ -37,6 +36,7 @@ test('should render html report git info metadata', async ({ runUITest }) => {
test('should work', async ({}) => {});
`
}, {
JENKINS_URL: '1',
BUILD_URL: 'https://playwright.dev',
});