feat(html report): show metadata (#34517)
This commit is contained in:
parent
6c2c90203e
commit
24f06ec1bf
|
|
@ -234,7 +234,9 @@ export default defineConfig({
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[Metadata]>
|
- type: ?<[Metadata]>
|
||||||
|
|
||||||
Metadata that will be put directly to the test report 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.
|
||||||
|
|
||||||
|
See also [`property: TestConfig.populateGitInfo`] that populates metadata.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
|
@ -242,7 +244,7 @@ Metadata that will be put directly to the test report serialized as JSON.
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
metadata: 'acceptance tests',
|
metadata: { title: 'acceptance tests' },
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -325,7 +327,9 @@ This path will serve as the base directory for each test file snapshot directory
|
||||||
* since: v1.51
|
* since: v1.51
|
||||||
- type: ?<[boolean]>
|
- type: ?<[boolean]>
|
||||||
|
|
||||||
Whether to populate [`property: TestConfig.metadata`] with Git info. The metadata will automatically appear in the HTML report and is available in Reporter API.
|
Whether to populate `'git.commit.info'` field of the [`property: TestConfig.metadata`] with Git commit info and CI/CD information.
|
||||||
|
|
||||||
|
This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
|
@ -647,7 +651,7 @@ export default defineConfig({
|
||||||
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
- `timeout` ?<[int]> How long to wait for the process to start up and be available in milliseconds. Defaults to 60000.
|
||||||
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
|
- `gracefulShutdown` ?<[Object]> How to shut down the process. If unspecified, the process group is forcefully `SIGKILL`ed. If set to `{ signal: 'SIGTERM', timeout: 500 }`, the process group is sent a `SIGTERM` signal, followed by `SIGKILL` if it doesn't exit within 500ms. You can also use `SIGINT` as the signal instead. A `0` timeout means no `SIGKILL` will be sent. Windows doesn't support `SIGTERM` and `SIGINT` signals, so this option is ignored on Windows. Note that shutting down a Docker container requires `SIGTERM`.
|
||||||
- `signal` <["SIGINT"|"SIGTERM"]>
|
- `signal` <["SIGINT"|"SIGTERM"]>
|
||||||
- `timeout` <[int]>
|
- `timeout` <[int]>
|
||||||
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
- `url` ?<[string]> The url on your http server that is expected to return a 2xx, 3xx, 400, 401, 402, or 403 status code when the server is ready to accept connections. Redirects (3xx status codes) are being followed and the new location is checked. Either `port` or `url` should be specified.
|
||||||
|
|
||||||
Launch a development web server (or multiple) during the tests.
|
Launch a development web server (or multiple) during the tests.
|
||||||
|
|
|
||||||
|
|
@ -69,22 +69,6 @@ export const blank = () => {
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'></svg>;
|
return <svg className='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'></svg>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const externalLink = () => {
|
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.604 1h4.146a.25.25 0 01.25.25v4.146a.25.25 0 01-.427.177L13.03 4.03 9.28 7.78a.75.75 0 01-1.06-1.06l3.75-3.75-1.543-1.543A.25.25 0 0110.604 1zM3.75 2A1.75 1.75 0 002 3.75v8.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 12.25v-3.5a.75.75 0 00-1.5 0v3.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-8.5a.25.25 0 01.25-.25h3.5a.75.75 0 000-1.5h-3.5z'></path></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const calendar = () => {
|
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z'></path></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const person = () => {
|
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 5a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm.061 3.073a4 4 0 10-5.123 0 6.004 6.004 0 00-3.431 5.142.75.75 0 001.498.07 4.5 4.5 0 018.99 0 .75.75 0 101.498-.07 6.005 6.005 0 00-3.432-5.142z'></path></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commit = () => {
|
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16'><path fillRule='evenodd' d='M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z'></path></svg>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const image = () => {
|
export const image = () => {
|
||||||
return <svg className='octicon' viewBox='0 0 48 48' version='1.1' width='20' height='20' aria-hidden='true'>
|
return <svg className='octicon' viewBox='0 0 48 48' version='1.1' width='20' height='20' aria-hidden='true'>
|
||||||
<path xmlns='http://www.w3.org/2000/svg' d='M11.85 32H36.2l-7.35-9.95-6.55 8.7-4.6-6.45ZM7 40q-1.2 0-2.1-.9Q4 38.2 4 37V11q0-1.2.9-2.1Q5.8 8 7 8h34q1.2 0 2.1.9.9.9.9 2.1v26q0 1.2-.9 2.1-.9.9-2.1.9Zm0-29v26-26Zm34 26V11H7v26Z'/>
|
<path xmlns='http://www.w3.org/2000/svg' d='M11.85 32H36.2l-7.35-9.95-6.55 8.7-4.6-6.45ZM7 40q-1.2 0-2.1-.9Q4 38.2 4 37V11q0-1.2.9-2.1Q5.8 8 7 8h34q1.2 0 2.1.9.9.9.9 2.1v26q0 1.2-.9 2.1-.9.9-2.1.9Zm0-29v26-26Zm34 26V11H7v26Z'/>
|
||||||
|
|
|
||||||
41
packages/html-reporter/src/metadataView.css
Normal file
41
packages/html-reporter/src/metadataView.css
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.metadata-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-view {
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-separator {
|
||||||
|
height: 1px;
|
||||||
|
border-bottom: 1px solid var(--color-border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-view .copy-value-container {
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-commit-info a {
|
||||||
|
color: var(--color-fg-default);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
@ -17,21 +17,19 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './colors.css';
|
import './colors.css';
|
||||||
import './common.css';
|
import './common.css';
|
||||||
import * as icons from './icons';
|
|
||||||
import { AutoChip } from './chip';
|
|
||||||
import './reportView.css';
|
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
|
import './metadataView.css';
|
||||||
|
import type { Metadata } from '@playwright/test';
|
||||||
|
import type { GitCommitInfo } from '@testIsomorphic/types';
|
||||||
|
import { CopyToClipboardContainer } from './copyToClipboard';
|
||||||
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
export type Metainfo = {
|
type MetadataEntries = [string, unknown][];
|
||||||
'revision.id'?: string;
|
|
||||||
'revision.author'?: string;
|
export function filterMetadata(metadata: Metadata): MetadataEntries {
|
||||||
'revision.email'?: string;
|
// TODO: do not plumb actualWorkers through metadata.
|
||||||
'revision.subject'?: string;
|
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||||
'revision.timestamp'?: number | Date;
|
}
|
||||||
'revision.link'?: string;
|
|
||||||
'ci.link'?: string;
|
|
||||||
'timestamp'?: number
|
|
||||||
};
|
|
||||||
|
|
||||||
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 } = {
|
||||||
|
|
@ -46,12 +44,12 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
||||||
override render() {
|
override render() {
|
||||||
if (this.state.error || this.state.errorInfo) {
|
if (this.state.error || this.state.errorInfo) {
|
||||||
return (
|
return (
|
||||||
<AutoChip header={'Commit Metainfo Error'} dataTestId='metadata-error'>
|
<div className='metadata-view p-3'>
|
||||||
<p>An error was encountered when trying to render Commit Metainfo. Please file a GitHub issue to report this error.</p>
|
<p>An error was encountered when trying to render metadata.</p>
|
||||||
<p>
|
<p>
|
||||||
<pre style={{ overflow: 'scroll' }}>{this.state.error?.message}<br/>{this.state.error?.stack}<br/>{this.state.errorInfo?.componentStack}</pre>
|
<pre style={{ overflow: 'scroll' }}>{this.state.error?.message}<br/>{this.state.error?.stack}<br/>{this.state.errorInfo?.componentStack}</pre>
|
||||||
</p>
|
</p>
|
||||||
</AutoChip>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,79 +57,50 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetadataView: React.FC<Metainfo> = metadata => <ErrorBoundary><InnerMetadataView {...metadata} /></ErrorBoundary>;
|
export const MetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||||
|
return <ErrorBoundary><InnerMetadataView metadataEntries={metadataEntries}/></ErrorBoundary>;
|
||||||
const InnerMetadataView: React.FC<Metainfo> = metadata => {
|
|
||||||
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoChip header={
|
|
||||||
<span>
|
|
||||||
{metadata['revision.id'] && <span style={{ float: 'right' }}>
|
|
||||||
{metadata['revision.id'].slice(0, 7)}
|
|
||||||
</span>}
|
|
||||||
{metadata['revision.subject'] || 'Commit Metainfo'}
|
|
||||||
</span>} initialExpanded={false} dataTestId='metadata-chip'>
|
|
||||||
{metadata['revision.subject'] &&
|
|
||||||
<MetadataViewItem
|
|
||||||
testId='revision.subject'
|
|
||||||
content={<span>{metadata['revision.subject']}</span>}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{metadata['revision.id'] &&
|
|
||||||
<MetadataViewItem
|
|
||||||
testId='revision.id'
|
|
||||||
content={<span>{metadata['revision.id']}</span>}
|
|
||||||
href={metadata['revision.link']}
|
|
||||||
icon='commit'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{(metadata['revision.author'] || metadata['revision.email']) &&
|
|
||||||
<MetadataViewItem
|
|
||||||
content={`${metadata['revision.author']} ${metadata['revision.email']}`}
|
|
||||||
icon='person'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{metadata['revision.timestamp'] &&
|
|
||||||
<MetadataViewItem
|
|
||||||
testId='revision.timestamp'
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{Intl.DateTimeFormat(undefined, { dateStyle: 'full' }).format(metadata['revision.timestamp'])}
|
|
||||||
{' '}
|
|
||||||
{Intl.DateTimeFormat(undefined, { timeStyle: 'long' }).format(metadata['revision.timestamp'])}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
icon='calendar'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{metadata['ci.link'] &&
|
|
||||||
<MetadataViewItem
|
|
||||||
content='CI/CD Logs'
|
|
||||||
href={metadata['ci.link']}
|
|
||||||
icon='externalLink'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{metadata['timestamp'] &&
|
|
||||||
<MetadataViewItem
|
|
||||||
content={<span style={{ color: 'var(--color-fg-subtle)' }}>
|
|
||||||
Report generated on {Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(metadata['timestamp'])}
|
|
||||||
</span>}></MetadataViewItem>
|
|
||||||
}
|
|
||||||
</AutoChip>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MetadataViewItem: React.FC<{ content: JSX.Element | string; icon?: keyof typeof icons, href?: string, testId?: string }> = ({ content, icon, href, testId }) => {
|
const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||||
return (
|
const gitCommitInfo = metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||||
<div className='my-1 hbox' data-testid={testId} >
|
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
||||||
<div className='mr-2'>
|
if (!gitCommitInfo && !entries.length)
|
||||||
{icons[icon || 'blank']()}
|
return null;
|
||||||
</div>
|
return <div className='metadata-view'>
|
||||||
<div style={{ flex: 1 }}>
|
{gitCommitInfo && <>
|
||||||
{href ? <a href={href} target='_blank' rel='noopener noreferrer'>{content}</a> : content}
|
<GitCommitInfoView info={gitCommitInfo}/>
|
||||||
|
{entries.length > 0 && <div className='metadata-separator' />}
|
||||||
|
</>}
|
||||||
|
{entries.map(([key, 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 <div className='m-1 ml-5' key={key}>
|
||||||
|
<span style={{ fontWeight: 'bold' }} title={key}>{key}</span>
|
||||||
|
{valueString && <CopyToClipboardContainer value={valueString}>: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span></CopyToClipboardContainer>}
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||||
|
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
||||||
|
const author = `${info['revision.author'] || ''}${email}`;
|
||||||
|
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='hbox pl-4 pr-2 git-commit-info' style={{ alignItems: 'center' }}>
|
||||||
|
<div className='vbox'>
|
||||||
|
<a className='m-2' href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<span title={info['revision.subject'] || ''}>{info['revision.subject'] || ''}</span>
|
||||||
|
</a>
|
||||||
|
<div className='hbox m-2 mt-1'>
|
||||||
|
<div className='mr-1'>{author}</div>
|
||||||
|
<div title={longTimestamp}> on {shortTimestamp}</div>
|
||||||
|
{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>
|
||||||
);
|
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<span title='View commit details'>{info['revision.id']?.slice(0, 7) || 'unknown'}</span>
|
||||||
|
</a>}
|
||||||
|
{!info['revision.link'] && !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||||
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import { HeaderView } from './headerView';
|
||||||
import { Route, SearchParamsContext } from './links';
|
import { Route, SearchParamsContext } from './links';
|
||||||
import type { LoadedReport } from './loadedReport';
|
import type { LoadedReport } from './loadedReport';
|
||||||
import './reportView.css';
|
import './reportView.css';
|
||||||
import type { Metainfo } from './metadataView';
|
|
||||||
import { MetadataView } from './metadataView';
|
|
||||||
import { TestCaseView } from './testCaseView';
|
import { TestCaseView } from './testCaseView';
|
||||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
|
|
@ -50,6 +48,7 @@ export const ReportView: React.FC<{
|
||||||
const searchParams = React.useContext(SearchParamsContext);
|
const searchParams = React.useContext(SearchParamsContext);
|
||||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||||
|
const [metadataVisible, setMetadataVisible] = React.useState(false);
|
||||||
|
|
||||||
const testIdToFileIdMap = React.useMemo(() => {
|
const testIdToFileIdMap = React.useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
|
|
@ -76,9 +75,8 @@ export const ReportView: React.FC<{
|
||||||
return <div className='htmlreport vbox px-4 pb-4'>
|
return <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>}
|
||||||
{report?.json().metadata && <MetadataView {...report?.json().metadata as Metainfo} />}
|
|
||||||
<Route predicate={testFilesRoutePredicate}>
|
<Route predicate={testFilesRoutePredicate}>
|
||||||
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} />
|
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} metadataVisible={metadataVisible} toggleMetadataVisible={() => setMetadataVisible(visible => !visible)}/>
|
||||||
<TestFilesView
|
<TestFilesView
|
||||||
tests={filteredTests.files}
|
tests={filteredTests.files}
|
||||||
expandedFiles={expandedFiles}
|
expandedFiles={expandedFiles}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import './testFileView.css';
|
||||||
import { msToString } from './utils';
|
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 { filterMetadata, MetadataView } from './metadataView';
|
||||||
|
|
||||||
export const TestFilesView: React.FC<{
|
export const TestFilesView: React.FC<{
|
||||||
tests: TestFileSummary[],
|
tests: TestFileSummary[],
|
||||||
|
|
@ -62,17 +64,24 @@ export const TestFilesView: React.FC<{
|
||||||
export const TestFilesHeader: React.FC<{
|
export const TestFilesHeader: React.FC<{
|
||||||
report: HTMLReport | undefined,
|
report: HTMLReport | undefined,
|
||||||
filteredStats?: FilteredStats,
|
filteredStats?: FilteredStats,
|
||||||
}> = ({ report, filteredStats }) => {
|
metadataVisible: boolean,
|
||||||
|
toggleMetadataVisible: () => void,
|
||||||
|
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||||
if (!report)
|
if (!report)
|
||||||
return;
|
return;
|
||||||
|
const metadataEntries = filterMetadata(report.metadata || {});
|
||||||
return <>
|
return <>
|
||||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||||
|
{metadataEntries.length > 0 && <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' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||||
<div style={{ flex: 'auto' }}></div>
|
<div style={{ flex: 'auto' }}></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-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 metadataEntries={metadataEntries}/>}
|
||||||
{!!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>}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
"@protocol/*": ["../protocol/src/*"],
|
"@protocol/*": ["../protocol/src/*"],
|
||||||
"@web/*": ["../web/src/*"],
|
"@web/*": ["../web/src/*"],
|
||||||
"@playwright/*": ["../playwright/src/*"],
|
"@playwright/*": ["../playwright/src/*"],
|
||||||
|
"@testIsomorphic/*": ["../playwright/src/isomorphic/*"],
|
||||||
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
"playwright-core/lib/*": ["../playwright-core/src/*"],
|
||||||
"playwright/lib/*": ["../playwright/src/*"],
|
"playwright/lib/*": ["../playwright/src/*"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
packages/playwright/src/isomorphic/types.d.ts
vendored
Normal file
25
packages/playwright/src/isomorphic/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* 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 interface GitCommitInfo {
|
||||||
|
'revision.id'?: string;
|
||||||
|
'revision.author'?: string;
|
||||||
|
'revision.email'?: string;
|
||||||
|
'revision.subject'?: string;
|
||||||
|
'revision.timestamp'?: number | Date;
|
||||||
|
'revision.link'?: string;
|
||||||
|
'ci.link'?: string;
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ 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';
|
||||||
|
|
||||||
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
|
@ -31,38 +32,23 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP
|
||||||
name: 'playwright:git-commit-info',
|
name: 'playwright:git-commit-info',
|
||||||
|
|
||||||
setup: async (config: FullConfig, configDir: string) => {
|
setup: async (config: FullConfig, configDir: string) => {
|
||||||
const info = {
|
const fromEnv = linksFromEnv();
|
||||||
...linksFromEnv(),
|
const fromCLI = await gitStatusFromCLI(options?.directory || configDir);
|
||||||
...options?.info ? options.info : await gitStatusFromCLI(options?.directory || configDir),
|
const info = { ...fromEnv, ...fromCLI };
|
||||||
timestamp: Date.now(),
|
if (info['revision.timestamp'] instanceof Date)
|
||||||
};
|
info['revision.timestamp'] = info['revision.timestamp'].getTime();
|
||||||
// Normalize dates
|
|
||||||
const timestamp = info['revision.timestamp'];
|
|
||||||
if (timestamp instanceof Date)
|
|
||||||
info['revision.timestamp'] = timestamp.getTime();
|
|
||||||
|
|
||||||
config.metadata = config.metadata || {};
|
config.metadata = config.metadata || {};
|
||||||
Object.assign(config.metadata, info);
|
config.metadata['git.commit.info'] = info;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GitCommitInfoPluginOptions {
|
interface GitCommitInfoPluginOptions {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
info?: Info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
function linksFromEnv(): Pick<GitCommitInfo, 'revision.link' | 'ci.link'> {
|
||||||
'revision.id'?: string;
|
|
||||||
'revision.author'?: string;
|
|
||||||
'revision.email'?: string;
|
|
||||||
'revision.subject'?: string;
|
|
||||||
'revision.timestamp'?: number | Date;
|
|
||||||
'revision.link'?: string;
|
|
||||||
'ci.link'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linksFromEnv = (): Pick<Info, 'revision.link' | 'ci.link'> => {
|
|
||||||
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
|
const out: { 'revision.link'?: string; 'ci.link'?: string; } = {};
|
||||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||||
if (process.env.BUILD_URL)
|
if (process.env.BUILD_URL)
|
||||||
|
|
@ -78,9 +64,9 @@ const linksFromEnv = (): Pick<Info, 'revision.link' | 'ci.link'> => {
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
||||||
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||||
return out;
|
return out;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const gitStatusFromCLI = async (gitDir: string): Promise<Info | undefined> => {
|
async function gitStatusFromCLI(gitDir: string): Promise<GitCommitInfo | undefined> {
|
||||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||||
const { code, stdout } = await spawnAsync(
|
const { code, stdout } = await spawnAsync(
|
||||||
'git',
|
'git',
|
||||||
|
|
@ -101,4 +87,4 @@ export const gitStatusFromCLI = async (gitDir: string): Promise<Info | undefined
|
||||||
'revision.subject': subject,
|
'revision.subject': subject,
|
||||||
'revision.timestamp': timestamp,
|
'revision.timestamp': timestamp,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
|
||||||
16
packages/playwright/types/test.d.ts
vendored
16
packages/playwright/types/test.d.ts
vendored
|
|
@ -1220,7 +1220,12 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
maxFailures?: number;
|
maxFailures?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata that will be put directly to the test report 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.
|
||||||
|
*
|
||||||
|
* See also
|
||||||
|
* [testConfig.populateGitInfo](https://playwright.dev/docs/api/class-testconfig#test-config-populate-git-info) that
|
||||||
|
* populates metadata.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
@ -1229,7 +1234,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* import { defineConfig } from '@playwright/test';
|
* import { defineConfig } from '@playwright/test';
|
||||||
*
|
*
|
||||||
* export default defineConfig({
|
* export default defineConfig({
|
||||||
* metadata: 'acceptance tests',
|
* metadata: { title: 'acceptance tests' },
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
|
@ -1294,8 +1299,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
outputDir?: string;
|
outputDir?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to populate [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata)
|
* Whether to populate `'git.commit.info'` field of the
|
||||||
* with Git info. The metadata will automatically appear in the HTML report and is available in Reporter API.
|
* [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata) with Git commit info
|
||||||
|
* and CI/CD information.
|
||||||
|
*
|
||||||
|
* This information will appear in the HTML and JSON reports and is available in the Reporter API.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -978,8 +978,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
|
|
||||||
await test.step('step', async () => {
|
await test.step('step', async () => {
|
||||||
testInfo.attachments.push({ name: 'attachment', body: 'content', contentType: 'text/plain' });
|
testInfo.attachments.push({ name: 'attachment', body: 'content', contentType: 'text/plain' });
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||||
|
|
@ -1095,7 +1095,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.spec.js': `
|
'a.spec.js': `
|
||||||
import { test as base, expect } from '@playwright/test';
|
import { test as base, expect } from '@playwright/test';
|
||||||
|
|
||||||
const test = base.extend({
|
const test = base.extend({
|
||||||
fixture1: [async ({}, use) => {
|
fixture1: [async ({}, use) => {
|
||||||
await use();
|
await use();
|
||||||
|
|
@ -1141,161 +1141,97 @@ for (const useIntermediateMergeReport of [true, false] as const) {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('gitCommitInfo plugin', () => {
|
test('should include metadata with populateGitInfo = true', async ({ runInlineTest, writeFiles, showReport, page }) => {
|
||||||
test('should include metadata with populateGitInfo = true', 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 {
|
||||||
import { test, expect } from '@playwright/test';
|
populateGitInfo: true,
|
||||||
export default { populateGitInfo: true };
|
metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] }
|
||||||
`,
|
};
|
||||||
'example.spec.ts': `
|
`,
|
||||||
import { test, expect } from '@playwright/test';
|
'example.spec.ts': `
|
||||||
test('sample', async ({}) => { expect(2).toBe(2); });
|
import { test, expect } from '@playwright/test';
|
||||||
`,
|
test('sample', async ({}) => { expect(2).toBe(2); });
|
||||||
};
|
`,
|
||||||
const baseDir = await writeFiles(files);
|
};
|
||||||
|
const baseDir = await writeFiles(files);
|
||||||
|
|
||||||
const execGit = async (args: string[]) => {
|
const execGit = async (args: string[]) => {
|
||||||
const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
|
const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
|
||||||
if (!!code)
|
if (!!code)
|
||||||
throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
|
throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
await execGit(['init']);
|
await execGit(['init']);
|
||||||
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
||||||
await execGit(['config', '--local', 'user.name', 'William']);
|
await execGit(['config', '--local', 'user.name', 'William']);
|
||||||
await execGit(['add', '*.ts']);
|
await execGit(['add', '*.ts']);
|
||||||
await execGit(['commit', '-m', 'awesome commit message']);
|
await execGit(['commit', '-m', 'chore(html): make this test look nice']);
|
||||||
|
|
||||||
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_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',
|
||||||
GITHUB_SHA: 'example-sha',
|
GITHUB_SHA: 'example-sha',
|
||||||
});
|
|
||||||
|
|
||||||
await showReport();
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
await page.click('text=awesome commit message');
|
|
||||||
await expect.soft(page.getByTestId('revision.id')).toContainText(/^[a-f\d]+$/i);
|
|
||||||
await expect.soft(page.getByTestId('revision.id').locator('a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
|
|
||||||
await expect.soft(page.getByTestId('revision.timestamp')).toContainText(/AM|PM/);
|
|
||||||
await expect.soft(page.locator('text=awesome commit message')).toHaveCount(2);
|
|
||||||
await expect.soft(page.locator('text=William')).toBeVisible();
|
|
||||||
await expect.soft(page.locator('text=shakespeare@example.local')).toBeVisible();
|
|
||||||
await expect.soft(page.locator('text=CI/CD Logs')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/actions/runs/example-run-id');
|
|
||||||
await expect.soft(page.locator('text=Report generated on')).toContainText(/AM|PM/);
|
|
||||||
await expect.soft(page.getByTestId('metadata-chip')).toBeVisible();
|
|
||||||
await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not include metadata with populateGitInfo = false', async ({ runInlineTest, showReport, page }) => {
|
await showReport();
|
||||||
const result = await runInlineTest({
|
|
||||||
'uncommitted.txt': `uncommitted file`,
|
|
||||||
'playwright.config.ts': `
|
|
||||||
export default { populateGitInfo: false };
|
|
||||||
`,
|
|
||||||
'example.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
|
||||||
`,
|
|
||||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined);
|
|
||||||
|
|
||||||
await showReport();
|
expect(result.exitCode).toBe(0);
|
||||||
|
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||||
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
|
- 'link "chore(html): make this test look nice"'
|
||||||
|
- text: /^William <shakespeare@example.local> on/
|
||||||
|
- link "logs"
|
||||||
|
- link /^[a-f0-9]{7}$/
|
||||||
|
- text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]'
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
test('should not include git metadata with populateGitInfo = false', async ({ runInlineTest, showReport, page }) => {
|
||||||
await expect.soft(page.locator('text="my sample test"')).toBeVisible();
|
const result = await runInlineTest({
|
||||||
await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
|
'playwright.config.ts': `
|
||||||
await expect.soft(page.getByTestId('metadata-chip')).not.toBeVisible();
|
export default { populateGitInfo: false };
|
||||||
});
|
`,
|
||||||
|
'example.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined);
|
||||||
|
|
||||||
test('should use explicitly supplied metadata', async ({ runInlineTest, showReport, page }) => {
|
await showReport();
|
||||||
const result = await runInlineTest({
|
|
||||||
'uncommitted.txt': `uncommitted file`,
|
|
||||||
'playwright.config.ts': `
|
|
||||||
import { gitCommitInfo } from 'playwright/lib/plugins';
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
const plugin = gitCommitInfo({
|
|
||||||
info: {
|
|
||||||
'revision.id': '1234567890',
|
|
||||||
'revision.subject': 'a better subject',
|
|
||||||
'revision.timestamp': new Date(),
|
|
||||||
'revision.author': 'William',
|
|
||||||
'revision.email': 'shakespeare@example.local',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export default { '@playwright/test': { plugins: [plugin] } };
|
|
||||||
`,
|
|
||||||
'example.spec.ts': `
|
|
||||||
import { gitCommitInfo } from 'playwright/lib/plugins';
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('sample', async ({}) => { expect(2).toBe(2); });
|
|
||||||
`,
|
|
||||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', GITHUB_SHA: 'example-sha' }, undefined);
|
|
||||||
|
|
||||||
await showReport();
|
expect(result.exitCode).toBe(0);
|
||||||
|
await expect.soft(page.getByRole('button', { name: 'Metadata' })).toBeHidden();
|
||||||
|
await expect.soft(page.locator('.metadata-view')).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
test('should show an error when metadata has invalid fields', async ({ runInlineTest, showReport, page }) => {
|
||||||
await page.click('text=a better subject');
|
const result = await runInlineTest({
|
||||||
await expect.soft(page.getByTestId('revision.id')).toContainText(/^[a-f\d]+$/i);
|
'uncommitted.txt': `uncommitted file`,
|
||||||
await expect.soft(page.getByTestId('revision.id').locator('a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
|
'playwright.config.ts': `
|
||||||
await expect.soft(page.getByTestId('revision.timestamp')).toContainText(/AM|PM/);
|
export default {
|
||||||
await expect.soft(page.locator('text=a better subject')).toHaveCount(2);
|
metadata: {
|
||||||
await expect.soft(page.locator('text=William')).toBeVisible();
|
'git.commit.info': { 'revision.timestamp': 'hi' }
|
||||||
await expect.soft(page.locator('text=shakespeare@example.local')).toBeVisible();
|
},
|
||||||
await expect.soft(page.locator('text=CI/CD Logs')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/actions/runs/example-run-id');
|
};
|
||||||
await expect.soft(page.locator('text=Report generated on')).toContainText(/AM|PM/);
|
`,
|
||||||
await expect.soft(page.getByTestId('metadata-chip')).toBeVisible();
|
'example.spec.ts': `
|
||||||
await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
|
import { test, expect } from '@playwright/test';
|
||||||
});
|
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||||
|
|
||||||
test('should not have metadata by default', async ({ runInlineTest, showReport, page }) => {
|
await showReport();
|
||||||
const result = await runInlineTest({
|
|
||||||
'uncommitted.txt': `uncommitted file`,
|
|
||||||
'playwright.config.ts': `
|
|
||||||
export default {};
|
|
||||||
`,
|
|
||||||
'example.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
|
||||||
`,
|
|
||||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }, undefined);
|
|
||||||
|
|
||||||
await showReport();
|
expect(result.exitCode).toBe(0);
|
||||||
|
await page.getByRole('button', { name: 'Metadata' }).click();
|
||||||
expect(result.exitCode).toBe(0);
|
await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(`
|
||||||
await expect.soft(page.locator('text="my sample test"')).toBeVisible();
|
- paragraph: An error was encountered when trying to render metadata.
|
||||||
await expect.soft(page.getByTestId('metadata-error')).not.toBeVisible();
|
`);
|
||||||
await expect.soft(page.getByTestId('metadata-chip')).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not include metadata if user supplies invalid values via metadata field', async ({ runInlineTest, showReport, page }) => {
|
|
||||||
const result = await runInlineTest({
|
|
||||||
'uncommitted.txt': `uncommitted file`,
|
|
||||||
'playwright.config.ts': `
|
|
||||||
export default {
|
|
||||||
metadata: {
|
|
||||||
'revision.timestamp': 'hi',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
`,
|
|
||||||
'example.spec.ts': `
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
|
||||||
`,
|
|
||||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
|
||||||
|
|
||||||
await showReport();
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
await expect.soft(page.locator('text="my sample test"')).toBeVisible();
|
|
||||||
await expect.soft(page.getByTestId('metadata-error')).toBeVisible();
|
|
||||||
await expect.soft(page.getByTestId('metadata-chip')).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report clashing folders', async ({ runInlineTest, useIntermediateMergeReport }) => {
|
test('should report clashing folders', async ({ runInlineTest, useIntermediateMergeReport }) => {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ 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['ci.link']);
|
console.log('ci.link:', config.metadata['git.commit.info']['ci.link']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue