chore: another iteration on gitCommit/gitDiff props (#34926)
This commit is contained in:
parent
17c4d8e5ec
commit
cd23a224f6
|
|
@ -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.
|
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**
|
**Usage**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,32 +20,10 @@ import './common.css';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
import './metadataView.css';
|
import './metadataView.css';
|
||||||
import type { Metadata } from '@playwright/test';
|
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 { CopyToClipboardContainer } from './copyToClipboard';
|
||||||
import { linkifyText } from '@web/renderUtils';
|
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 }> {
|
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||||
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
override state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
||||||
error: null,
|
error: null,
|
||||||
|
|
@ -72,23 +50,22 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetadataView = () => {
|
export const MetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||||
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
return <ErrorBoundary><InnerMetadataView metadata={params.metadata}/></ErrorBoundary>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InnerMetadataView = () => {
|
const InnerMetadataView: React.FC<{ metadata: Metadata }> = params => {
|
||||||
const metadataEntries = useMetadata();
|
const commitInfo = params.metadata as MetadataWithCommitInfo;
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const otherEntries = Object.entries(params.metadata).filter(([key]) => !ignoreKeys.has(key));
|
||||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
const hasMetadata = commitInfo.ci || commitInfo.gitCommit || otherEntries.length > 0;
|
||||||
if (!gitCommitInfo && !entries.length)
|
if (!hasMetadata)
|
||||||
return null;
|
return;
|
||||||
return <div className='metadata-view'>
|
return <div className='metadata-view'>
|
||||||
{gitCommitInfo && <>
|
{commitInfo.ci && <CiInfoView info={commitInfo.ci}/>}
|
||||||
<GitCommitInfoView info={gitCommitInfo}/>
|
{commitInfo.gitCommit && <GitCommitInfoView link={commitInfo.ci?.commitHref} info={commitInfo.gitCommit}/>}
|
||||||
{entries.length > 0 && <div className='metadata-separator' />}
|
{otherEntries.length > 0 && (commitInfo.gitCommit || commitInfo.ci) && <div className='metadata-separator' />}
|
||||||
</>}
|
|
||||||
<div className='metadata-section metadata-properties' role='list'>
|
<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 valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||||
return (
|
return (
|
||||||
|
|
@ -104,20 +81,24 @@ const InnerMetadataView = () => {
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
const CiInfoView: React.FC<{ info: CIInfo }> = ({ info }) => {
|
||||||
const email = info.revision?.email ? ` <${info.revision?.email}>` : '';
|
const link = info.commitHref;
|
||||||
const author = `${info.revision?.author || ''}${email}`;
|
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 || '';
|
const GitCommitInfoView: React.FC<{ link?: string, info: GitCommitInfo }> = ({ link, info }) => {
|
||||||
let link = info.revision?.link;
|
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'>
|
return <div className='metadata-section' role='list'>
|
||||||
<div role='listitem'>
|
<div role='listitem'>
|
||||||
{link ? (
|
{link ? (
|
||||||
|
|
@ -131,12 +112,13 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||||
<div role='listitem' className='hbox'>
|
<div role='listitem' className='hbox'>
|
||||||
<span className='mr-1'>{author}</span>
|
<span className='mr-1'>{author}</span>
|
||||||
<span title={longTimestamp}> on {shortTimestamp}</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>
|
||||||
</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;
|
||||||
|
};
|
||||||
|
|
|
||||||
29
packages/html-reporter/src/reportContext.tsx
Normal file
29
packages/html-reporter/src/reportContext.tsx
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ import './reportView.css';
|
||||||
import { TestCaseView } from './testCaseView';
|
import { TestCaseView } from './testCaseView';
|
||||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
import { MetadataProvider } from './metadataView';
|
import { HTMLReportContextProvider } from './reportContext';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -73,7 +73,7 @@ export const ReportView: React.FC<{
|
||||||
return result;
|
return result;
|
||||||
}, [report, filter]);
|
}, [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>
|
<main>
|
||||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||||
<Route predicate={testFilesRoutePredicate}>
|
<Route predicate={testFilesRoutePredicate}>
|
||||||
|
|
@ -89,7 +89,7 @@ export const ReportView: React.FC<{
|
||||||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||||
</Route>
|
</Route>
|
||||||
</main>
|
</main>
|
||||||
</div></MetadataProvider>;
|
</div></HTMLReportContextProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestCaseViewLoader: React.FC<{
|
const TestCaseViewLoader: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,14 @@ import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import type { TestResult } from './types';
|
import type { TestResult } from './types';
|
||||||
import { fixTestPrompt } from '@web/components/prompts';
|
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 (
|
return (
|
||||||
<CodeSnippet code={error} testId={testId}>
|
<CodeSnippet code={error} testId={testId}>
|
||||||
<div style={{ float: 'right', margin: 10 }}>
|
<div style={{ float: 'right', margin: 10 }}>
|
||||||
|
|
@ -47,12 +52,13 @@ const PromptButton: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
result?: TestResult;
|
result?: TestResult;
|
||||||
}> = ({ error, result }) => {
|
}> = ({ error, result }) => {
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const report = useHTMLReport();
|
||||||
|
const commitInfo = report?.metadata as MetadataWithCommitInfo | undefined;
|
||||||
const prompt = React.useMemo(() => fixTestPrompt(
|
const prompt = React.useMemo(() => fixTestPrompt(
|
||||||
error,
|
error,
|
||||||
gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff,
|
commitInfo?.gitDiff,
|
||||||
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
||||||
), [gitCommitInfo, result, error]);
|
), [commitInfo, result, error]);
|
||||||
|
|
||||||
const [copied, setCopied] = React.useState(false);
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { msToString } from './utils';
|
||||||
import { AutoChip } from './chip';
|
import { AutoChip } from './chip';
|
||||||
import { TestErrorView } from './testErrorView';
|
import { TestErrorView } from './testErrorView';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import { MetadataView, useMetadata } from './metadataView';
|
import { isMetadataEmpty, MetadataView } from './metadataView';
|
||||||
|
|
||||||
export const TestFilesView: React.FC<{
|
export const TestFilesView: React.FC<{
|
||||||
tests: TestFileSummary[],
|
tests: TestFileSummary[],
|
||||||
|
|
@ -67,13 +67,12 @@ export const TestFilesHeader: React.FC<{
|
||||||
metadataVisible: boolean,
|
metadataVisible: boolean,
|
||||||
toggleMetadataVisible: () => void,
|
toggleMetadataVisible: () => void,
|
||||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||||
const metadataEntries = useMetadata();
|
|
||||||
if (!report)
|
if (!report)
|
||||||
return null;
|
return null;
|
||||||
return <>
|
return <>
|
||||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||||
<div className='test-file-header-info'>
|
<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
|
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||||
</div>}
|
</div>}
|
||||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</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-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 data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
{metadataVisible && <MetadataView/>}
|
{metadataVisible && <MetadataView metadata={report.metadata}/>}
|
||||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
|
||||||
57
packages/playwright/src/isomorphic/types.d.ts
vendored
57
packages/playwright/src/isomorphic/types.d.ts
vendored
|
|
@ -14,23 +14,40 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GitCommitInfo {
|
export type GitCommitInfo = {
|
||||||
revision?: {
|
shortHash: string;
|
||||||
id?: string;
|
hash: string;
|
||||||
author?: string;
|
subject: string;
|
||||||
email?: string;
|
body: string;
|
||||||
subject?: string;
|
author: {
|
||||||
timestamp?: number;
|
name: string;
|
||||||
link?: string;
|
email: string;
|
||||||
diff?: string;
|
time: number;
|
||||||
},
|
};
|
||||||
pull_request?: {
|
committer: {
|
||||||
link?: string;
|
name: string;
|
||||||
diff?: string;
|
email: string
|
||||||
base?: string;
|
time: number;
|
||||||
title?: string;
|
};
|
||||||
},
|
branch: string;
|
||||||
ci?: {
|
};
|
||||||
link?: 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;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,119 +14,139 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import { spawnAsync } from 'playwright-core/lib/utils';
|
||||||
|
|
||||||
import { createGuid, spawnAsync } from 'playwright-core/lib/utils';
|
|
||||||
|
|
||||||
import type { TestRunnerPlugin } from './';
|
import type { TestRunnerPlugin } from './';
|
||||||
import type { FullConfig } from '../../types/testReporter';
|
import type { FullConfig } from '../../types/testReporter';
|
||||||
import type { FullConfigInternal } from '../common/config';
|
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) => {
|
export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
|
||||||
const commitProperty = fullConfig.config.metadata['git.commit.info'];
|
fullConfig.plugins.push({ factory: gitCommitInfoPlugin });
|
||||||
if (commitProperty && typeof commitProperty === 'object' && Object.keys(commitProperty).length === 0)
|
|
||||||
fullConfig.plugins.push({ factory: gitCommitInfo });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
type GitCommitInfoPluginOptions = {
|
||||||
|
directory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
|
||||||
return {
|
return {
|
||||||
name: 'playwright:git-commit-info',
|
name: 'playwright:git-commit-info',
|
||||||
|
|
||||||
setup: async (config: FullConfig, configDir: string) => {
|
setup: async (config: FullConfig, configDir: string) => {
|
||||||
const commitInfo = await linksFromEnv();
|
const metadata = config.metadata as UserMetadataWithCommitInfo;
|
||||||
await enrichStatusFromCLI(options?.directory || configDir, commitInfo);
|
const ci = ciInfo();
|
||||||
config.metadata = config.metadata || {};
|
if (!metadata.ci && ci)
|
||||||
config.metadata['git.commit.info'] = commitInfo;
|
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 {
|
function ciInfo(): CIInfo | undefined {
|
||||||
directory?: string;
|
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> {
|
async function gitCommitInfo(gitDir: string): Promise<GitCommitInfo | undefined> {
|
||||||
const out: GitCommitInfo = {};
|
const separator = `---786eec917292---`;
|
||||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
const tokens = [
|
||||||
if (process.env.BUILD_URL) {
|
'%H', // commit hash
|
||||||
out.ci = out.ci || {};
|
'%h', // abbreviated commit hash
|
||||||
out.ci.link = process.env.BUILD_URL;
|
'%s', // subject
|
||||||
}
|
'%B', // raw body (unwrapped subject and body)
|
||||||
// GitLab: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
|
'%an', // author name
|
||||||
if (process.env.CI_PROJECT_URL && process.env.CI_COMMIT_SHA) {
|
'%ae', // author email
|
||||||
out.revision = out.revision || {};
|
'%at', // author date, UNIX timestamp
|
||||||
out.revision.link = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
|
'%cn', // committer name
|
||||||
}
|
'%ce', // committer email
|
||||||
if (process.env.CI_JOB_URL) {
|
'%ct', // committer date, UNIX timestamp
|
||||||
out.ci = out.ci || {};
|
'', // branch
|
||||||
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)}:`;
|
|
||||||
const commitInfoResult = await spawnAsync(
|
const commitInfoResult = await spawnAsync(
|
||||||
'git',
|
`git log -1 --pretty=format:"${tokens.join(separator)}" && git rev-parse --abbrev-ref HEAD`, [],
|
||||||
['show', '-s', `--format=%H${separator}%s${separator}%an${separator}%ae${separator}%ct`, 'HEAD'],
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS, shell: true }
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
|
||||||
);
|
);
|
||||||
if (commitInfoResult.code)
|
if (commitInfoResult.code)
|
||||||
return;
|
return undefined;
|
||||||
const showOutput = commitInfoResult.stdout.trim();
|
const showOutput = commitInfoResult.stdout.trim();
|
||||||
const [id, subject, author, email, rawTimestamp] = showOutput.split(separator);
|
const [hash, shortHash, subject, body, authorName, authorEmail, authorTime, committerName, committerEmail, committerTime, branch] = showOutput.split(separator);
|
||||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
|
||||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
|
||||||
|
|
||||||
commitInfo.revision = {
|
return {
|
||||||
...commitInfo.revision,
|
shortHash,
|
||||||
id,
|
hash,
|
||||||
author,
|
|
||||||
email,
|
|
||||||
subject,
|
subject,
|
||||||
timestamp,
|
body,
|
||||||
|
author: {
|
||||||
|
name: authorName,
|
||||||
|
email: authorEmail,
|
||||||
|
time: +authorTime * 1000,
|
||||||
|
},
|
||||||
|
committer: {
|
||||||
|
name: committerName,
|
||||||
|
email: committerEmail,
|
||||||
|
time: +committerTime * 1000,
|
||||||
|
},
|
||||||
|
branch: branch.trim(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gitDiff(gitDir: string, ci?: CIInfo): Promise<string | undefined> {
|
||||||
|
const diffLimit = 100_000;
|
||||||
|
const baseHash = ci?.baseHash ?? 'HEAD~1';
|
||||||
|
|
||||||
const diffLimit = 1_000_000; // 1MB
|
|
||||||
if (commitInfo.pull_request?.base) {
|
|
||||||
const pullDiffResult = await spawnAsync(
|
const pullDiffResult = await spawnAsync(
|
||||||
'git',
|
'git',
|
||||||
['diff', commitInfo.pull_request?.base],
|
['diff', baseHash],
|
||||||
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
{ stdio: 'pipe', cwd: gitDir, timeout: GIT_OPERATIONS_TIMEOUT_MS }
|
||||||
);
|
);
|
||||||
if (!pullDiffResult.code)
|
if (!pullDiffResult.code)
|
||||||
commitInfo.pull_request!.diff = pullDiffResult.stdout.substring(0, diffLimit);
|
return 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,3 @@ export type TestRunnerPluginRegistration = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export { webServer } from './webServerPlugin';
|
export { webServer } from './webServerPlugin';
|
||||||
export { gitCommitInfo } from './gitCommitInfoPlugin';
|
|
||||||
|
|
|
||||||
6
packages/playwright/types/test.d.ts
vendored
6
packages/playwright/types/test.d.ts
vendored
|
|
@ -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
|
* 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.
|
* 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
|
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
|
||||||
* environments.
|
* generation.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,20 @@ import type { StackFrame } from '@protocol/channels';
|
||||||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||||
import { attachmentURL } from './attachmentsTab';
|
import { attachmentURL } from './attachmentsTab';
|
||||||
import { fixTestPrompt } from '@web/components/prompts';
|
import { fixTestPrompt } from '@web/components/prompts';
|
||||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||||
import { AIConversation } from './aiConversation';
|
import { AIConversation } from './aiConversation';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
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';
|
||||||
|
|
||||||
const GitCommitInfoContext = React.createContext<GitCommitInfo | undefined>(undefined);
|
const CommitInfoContext = React.createContext<MetadataWithCommitInfo | undefined>(undefined);
|
||||||
|
|
||||||
export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) {
|
export function CommitInfoProvider({ children, commitInfo }: React.PropsWithChildren<{ commitInfo: MetadataWithCommitInfo }>) {
|
||||||
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
|
return <CommitInfoContext.Provider value={commitInfo}>{children}</CommitInfoContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGitCommitInfo() {
|
export function useCommitInfo() {
|
||||||
return React.useContext(GitCommitInfoContext);
|
return React.useContext(CommitInfoContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePageSnapshot(actions: modelUtil.ActionTraceEventInContext[]) {
|
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 }) {
|
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 [showLLM, setShowLLM] = React.useState(false);
|
||||||
const llmAvailable = useIsLLMAvailable();
|
const llmAvailable = useIsLLMAvailable();
|
||||||
const gitCommitInfo = useGitCommitInfo();
|
const metadata = useCommitInfo();
|
||||||
const diff = gitCommitInfo?.pull_request?.diff ?? gitCommitInfo?.revision?.diff;
|
|
||||||
|
|
||||||
let location: string | undefined;
|
let location: string | undefined;
|
||||||
let longLocation: string | undefined;
|
let longLocation: string | undefined;
|
||||||
|
|
@ -127,8 +126,8 @@ function Error({ message, error, errorId, sdkLanguage, pageSnapshot, revealInSou
|
||||||
</div>}
|
</div>}
|
||||||
<span style={{ position: 'absolute', right: '5px' }}>
|
<span style={{ position: 'absolute', right: '5px' }}>
|
||||||
{llmAvailable
|
{llmAvailable
|
||||||
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={diff} pageSnapshot={pageSnapshot} />
|
? <FixWithAIButton conversationId={errorId} onChange={setShowLLM} value={showLLM} error={message} diff={metadata?.gitDiff} pageSnapshot={pageSnapshot} />
|
||||||
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={diff} />}
|
: <CopyPromptButton error={message} pageSnapshot={pageSnapshot} diff={metadata?.gitDiff} />}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ import { TestListView } from './uiModeTestListView';
|
||||||
import { TraceView } from './uiModeTraceView';
|
import { TraceView } from './uiModeTraceView';
|
||||||
import { SettingsView } from './settingsView';
|
import { SettingsView } from './settingsView';
|
||||||
import { DefaultSettingsView } from './defaultSettingsView';
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
import { GitCommitInfoProvider } from './errorsTab';
|
import { CommitInfoProvider } from './errorsTab';
|
||||||
import { LLMProvider } from './llm';
|
import { LLMProvider } from './llm';
|
||||||
|
import type { MetadataWithCommitInfo } from '@testIsomorphic/types';
|
||||||
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
|
|
@ -432,7 +433,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
||||||
<GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}>
|
<CommitInfoProvider commitInfo={testModel?.config.metadata as MetadataWithCommitInfo}>
|
||||||
<TraceView
|
<TraceView
|
||||||
pathSeparator={queryParams.pathSeparator}
|
pathSeparator={queryParams.pathSeparator}
|
||||||
item={selectedItem}
|
item={selectedItem}
|
||||||
|
|
@ -440,7 +441,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
revealSource={revealSource}
|
revealSource={revealSource}
|
||||||
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||||
/>
|
/>
|
||||||
</GitCommitInfoProvider>
|
</CommitInfoProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
sidebar={<div className='vbox ui-mode-sidebar'>
|
sidebar={<div className='vbox ui-mode-sidebar'>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
const files = {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
'uncommitted.txt': `uncommitted file`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
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': `
|
'example.spec.ts': `
|
||||||
|
|
@ -1219,6 +1219,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
GITHUB_SHA: 'example-sha',
|
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 = {
|
const files = {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
'uncommitted.txt': `uncommitted file`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
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': `
|
'example.spec.ts': `
|
||||||
|
|
@ -1281,6 +1282,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_RUN_ID: 'example-run-id',
|
GITHUB_RUN_ID: 'example-run-id',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
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(`
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
- list:
|
- list:
|
||||||
- listitem:
|
- listitem:
|
||||||
- link "My PR"
|
- link "https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha"
|
||||||
- listitem:
|
|
||||||
- text: /William <shakespeare@example\\.local>/
|
|
||||||
- link "Logs"
|
|
||||||
- list:
|
- list:
|
||||||
- listitem: "foo : value1"
|
- listitem: "foo : value1"
|
||||||
- listitem: "bar : {\\"prop\\":\\"value2\\"}"
|
- 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({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {};
|
export default {};
|
||||||
|
|
@ -1330,7 +1329,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
metadata: {
|
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': `
|
'playwright.config.ts': `
|
||||||
export default {
|
export default {
|
||||||
metadata: {
|
metadata: {
|
||||||
'git.commit.info': {},
|
|
||||||
foo: 'value1',
|
foo: 'value1',
|
||||||
bar: { prop: 'value2' },
|
bar: { prop: 'value2' },
|
||||||
baz: ['value3', 123]
|
baz: ['value3', 123]
|
||||||
|
|
@ -2799,6 +2797,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
const result = await runInlineTest(files, { reporter: 'dot,html' }, {
|
||||||
PLAYWRIGHT_HTML_OPEN: 'never',
|
PLAYWRIGHT_HTML_OPEN: 'never',
|
||||||
|
GITHUB_ACTIONS: '1',
|
||||||
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test',
|
||||||
GITHUB_RUN_ID: 'example-run-id',
|
GITHUB_RUN_ID: 'example-run-id',
|
||||||
GITHUB_SERVER_URL: 'https://playwright.dev',
|
GITHUB_SERVER_URL: 'https://playwright.dev',
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,13 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
||||||
'reporter.ts': `
|
'reporter.ts': `
|
||||||
module.exports = class Reporter {
|
module.exports = class Reporter {
|
||||||
onBegin(config, suite) {
|
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': `
|
'playwright.config.ts': `
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
metadata: { 'git.commit.info': {} },
|
|
||||||
reporter: './reporter.ts',
|
reporter: './reporter.ts',
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
@ -37,6 +36,7 @@ test('should render html report git info metadata', async ({ runUITest }) => {
|
||||||
test('should work', async ({}) => {});
|
test('should work', async ({}) => {});
|
||||||
`
|
`
|
||||||
}, {
|
}, {
|
||||||
|
JENKINS_URL: '1',
|
||||||
BUILD_URL: 'https://playwright.dev',
|
BUILD_URL: 'https://playwright.dev',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue