feat: rewrite gitCommitInfo plugin, drop GlobalInfo & attachments (#13837)
This commit is contained in:
parent
ed344a882b
commit
3b3cad7d69
|
|
@ -1,131 +0,0 @@
|
||||||
# class: GlobalInfo
|
|
||||||
* langs: js
|
|
||||||
|
|
||||||
`GlobalInfo` contains information on the overall test run. The information spans projects and tests. Some reporters show global info.
|
|
||||||
|
|
||||||
You can write to GlobalInfo via your Global Setup hook, and read from it in a [Custom Reporter](../test-reporters.md):
|
|
||||||
|
|
||||||
```js js-flavor=js
|
|
||||||
// global-setup.js
|
|
||||||
module.exports = async (config, info) => {
|
|
||||||
await info.attach('agent.config.txt', { path: './agent.config.txt' });
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
```js js-flavor=ts
|
|
||||||
// global-setup.ts
|
|
||||||
import { chromium, FullConfig, GlobalInfo } from '@playwright/test';
|
|
||||||
|
|
||||||
async function globalSetup(config: FullConfig, info: GlobalInfo) {
|
|
||||||
await info.attach('agent.config.txt', { path: './agent.config.txt' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default globalSetup;
|
|
||||||
```
|
|
||||||
|
|
||||||
Access the attachments from the Root Suite in the Reporter:
|
|
||||||
|
|
||||||
```js js-flavor=js
|
|
||||||
// my-awesome-reporter.js
|
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
/** @implements {import('@playwright/test/reporter').Reporter} */
|
|
||||||
class MyReporter {
|
|
||||||
onBegin(config, suite) {
|
|
||||||
this._suite = suite;
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnd(result) {
|
|
||||||
console.log(`Finished the run with ${this._suite.attachments.length} global attachments!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MyReporter;
|
|
||||||
```
|
|
||||||
|
|
||||||
```js js-flavor=ts
|
|
||||||
// my-awesome-reporter.ts
|
|
||||||
import { Reporter } from '@playwright/test/reporter';
|
|
||||||
|
|
||||||
class MyReporter implements Reporter {
|
|
||||||
private _suite;
|
|
||||||
|
|
||||||
onBegin(config, suite) {
|
|
||||||
this._suite = suite;
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnd(result) {
|
|
||||||
console.log(`Finished the run with ${this._suite.attachments.length} global attachments!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default MyReporter;
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, specify `globalSetup` in the configuration file and `reporter`:
|
|
||||||
|
|
||||||
```js js-flavor=js
|
|
||||||
// playwright.config.js
|
|
||||||
// @ts-check
|
|
||||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
|
||||||
const config = {
|
|
||||||
globalSetup: require.resolve('./global-setup'),
|
|
||||||
reporter: require.resolve('./my-awesome-reporter'),
|
|
||||||
};
|
|
||||||
module.exports = config;
|
|
||||||
```
|
|
||||||
|
|
||||||
```js js-flavor=ts
|
|
||||||
// playwright.config.ts
|
|
||||||
import { PlaywrightTestConfig } from '@playwright/test';
|
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
|
||||||
globalSetup: require.resolve('./global-setup'),
|
|
||||||
reporter: require.resolve('./my-awesome-reporter'),
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
```
|
|
||||||
|
|
||||||
See [`TestInfo`](./class-testinfo.md) for related attachment functionality scoped to the test-level.
|
|
||||||
|
|
||||||
## method: GlobalInfo.attachments
|
|
||||||
- type: <[Array]<[Object]>>
|
|
||||||
- `name` <[string]> Attachment name.
|
|
||||||
- `contentType` <[string]> Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
- `path` ?<[string]> Optional path on the filesystem to the attached file.
|
|
||||||
- `body` ?<[Buffer]> Optional attachment body used instead of a file.
|
|
||||||
|
|
||||||
The list of files or buffers attached to the overall test run. Some reporters show global attachments.
|
|
||||||
|
|
||||||
To add an attachment, use [`method: GlobalInfo.attach`]. See [`property: TestInfo.attachments`] if you are looking for test-scoped attachments.
|
|
||||||
|
|
||||||
## async method: GlobalInfo.attach
|
|
||||||
|
|
||||||
Attach a value or a file from disk to the overall test run. Some reporters show global attachments. Either [`option: path`] or [`option: body`] must be specified, but not both.
|
|
||||||
|
|
||||||
See [`method: TestInfo.attach`] if you are looking for test-scoped attachments.
|
|
||||||
|
|
||||||
:::note
|
|
||||||
[`method: GlobalInfo.attach`] automatically takes care of copying attached files to a
|
|
||||||
location that is accessible to reporters. You can safely remove the attachment
|
|
||||||
after awaiting the attach call.
|
|
||||||
:::
|
|
||||||
|
|
||||||
### param: GlobalInfo.attach.name
|
|
||||||
- `name` <[string]>
|
|
||||||
|
|
||||||
Attachment name.
|
|
||||||
|
|
||||||
### option: GlobalInfo.attach.body
|
|
||||||
- `body` ?<[string]|[Buffer]>
|
|
||||||
|
|
||||||
Attachment body. Mutually exclusive with [`option: path`].
|
|
||||||
|
|
||||||
### option: GlobalInfo.attach.contentType
|
|
||||||
- `contentType` ?<[string]>
|
|
||||||
|
|
||||||
Optional content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`. If omitted, content type is inferred based on the [`option: path`], or defaults to `text/plain` for [string] attachments and `application/octet-stream` for [Buffer] attachments.
|
|
||||||
|
|
||||||
### option: GlobalInfo.attach.path
|
|
||||||
- `path` ?<[string]>
|
|
||||||
|
|
||||||
Path on the filesystem to the attached file. Mutually exclusive with [`option: body`].
|
|
||||||
|
|
@ -249,9 +249,9 @@ export default config;
|
||||||
```
|
```
|
||||||
|
|
||||||
## property: TestConfig.metadata
|
## property: TestConfig.metadata
|
||||||
- type: ?<[any]>
|
- type: ?<[Metadata]>
|
||||||
|
|
||||||
Any JSON-serializable metadata that will be put directly to the test report.
|
Metadata that will be put directly to the test report serialized as JSON.
|
||||||
|
|
||||||
## property: TestConfig.name
|
## property: TestConfig.name
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,3 @@ Suite title.
|
||||||
- returns: <[Array]<[string]>>
|
- returns: <[Array]<[string]>>
|
||||||
|
|
||||||
Returns a list of titles from the root down to this suite.
|
Returns a list of titles from the root down to this suite.
|
||||||
|
|
||||||
## property: Suite.attachments
|
|
||||||
- type: <[Array]<[Object]>>
|
|
||||||
- `name` <[string]> Attachment name.
|
|
||||||
- `contentType` <[string]> Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
- `path` ?<[string]> Optional path on the filesystem to the attached file.
|
|
||||||
- `body` ?<[Buffer]> Optional attachment body used instead of a file.
|
|
||||||
|
|
||||||
The list of files or buffers attached to the suite. Root suite has attachments populated by [`method: GlobalInfo.attach`].
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ export const Chip: React.FC<{
|
||||||
noInsets?: boolean,
|
noInsets?: boolean,
|
||||||
setExpanded?: (expanded: boolean) => void,
|
setExpanded?: (expanded: boolean) => void,
|
||||||
children?: any,
|
children?: any,
|
||||||
}> = ({ header, expanded, setExpanded, children, noInsets }) => {
|
dataTestId?: string,
|
||||||
return <div className='chip'>
|
}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId }) => {
|
||||||
|
return <div className='chip' data-test-id={dataTestId}>
|
||||||
<div
|
<div
|
||||||
className={'chip-header' + (setExpanded ? ' expanded-' + expanded : '')}
|
className={'chip-header' + (setExpanded ? ' expanded-' + expanded : '')}
|
||||||
onClick={() => setExpanded?.(!expanded)}
|
onClick={() => setExpanded?.(!expanded)}
|
||||||
|
|
@ -45,13 +46,15 @@ export const AutoChip: React.FC<{
|
||||||
initialExpanded?: boolean,
|
initialExpanded?: boolean,
|
||||||
noInsets?: boolean,
|
noInsets?: boolean,
|
||||||
children?: any,
|
children?: any,
|
||||||
}> = ({ header, initialExpanded, noInsets, children }) => {
|
dataTestId?: string,
|
||||||
|
}> = ({ header, initialExpanded, noInsets, children, dataTestId }) => {
|
||||||
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
|
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
|
||||||
return <Chip
|
return <Chip
|
||||||
header={header}
|
header={header}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
setExpanded={setExpanded}
|
setExpanded={setExpanded}
|
||||||
noInsets={noInsets}
|
noInsets={noInsets}
|
||||||
|
dataTestId={dataTestId}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Chip>;
|
</Chip>;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { HTMLReport, TestAttachment } from '@playwright-test/reporters/html';
|
import type { HTMLReport } from '@playwright-test/reporters/html';
|
||||||
import type zip from '@zip.js/zip.js';
|
import type zip from '@zip.js/zip.js';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import zipImport from '@zip.js/zip.js/dist/zip-no-worker-inflate.min.js';
|
import zipImport from '@zip.js/zip.js/dist/zip-no-worker-inflate.min.js';
|
||||||
|
|
@ -26,59 +26,6 @@ import { ReportView } from './reportView';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const zipjs = zipImport as typeof zip;
|
const zipjs = zipImport as typeof zip;
|
||||||
|
|
||||||
export type Metadata = Partial<{
|
|
||||||
'generatedAt': number;
|
|
||||||
'revision.id': string;
|
|
||||||
'revision.author': string;
|
|
||||||
'revision.email': string;
|
|
||||||
'revision.subject': string;
|
|
||||||
'revision.timestamp': number;
|
|
||||||
'revision.link': string;
|
|
||||||
'revision.localPendingChanges': boolean;
|
|
||||||
'ci.link': string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const extractMetadata = (attachments: TestAttachment[]): Metadata | undefined => {
|
|
||||||
// The last plugin to register for a given key will take precedence
|
|
||||||
attachments = [...attachments];
|
|
||||||
attachments.reverse();
|
|
||||||
const field = (name: string) => attachments.find(({ name: n }) => n === name)?.body;
|
|
||||||
const fieldAsJSON = (name: string) => {
|
|
||||||
const raw = field(name);
|
|
||||||
if (raw !== undefined)
|
|
||||||
return JSON.parse(raw);
|
|
||||||
};
|
|
||||||
const fieldAsNumber = (name: string) => {
|
|
||||||
const v = fieldAsJSON(name);
|
|
||||||
if (v !== undefined && typeof v !== 'number')
|
|
||||||
throw new Error(`Invalid value for field '${name}'. Expected type 'number', but got ${typeof v}.`);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
};
|
|
||||||
const fieldAsBool = (name: string) => {
|
|
||||||
const v = fieldAsJSON(name);
|
|
||||||
if (v !== undefined && typeof v !== 'boolean')
|
|
||||||
throw new Error(`Invalid value for field '${name}'. Expected type 'boolean', but got ${typeof v}.`);
|
|
||||||
|
|
||||||
return v;
|
|
||||||
};
|
|
||||||
|
|
||||||
const out = {
|
|
||||||
'generatedAt': fieldAsNumber('generatedAt'),
|
|
||||||
'revision.id': field('revision.id'),
|
|
||||||
'revision.author': field('revision.author'),
|
|
||||||
'revision.email': field('revision.email'),
|
|
||||||
'revision.subject': field('revision.subject'),
|
|
||||||
'revision.timestamp': fieldAsNumber('revision.timestamp'),
|
|
||||||
'revision.link': field('revision.link'),
|
|
||||||
'revision.localPendingChanges': fieldAsBool('revision.localPendingChanges'),
|
|
||||||
'ci.link': field('ci.link'),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.entries(out).filter(([_, v]) => v !== undefined).length)
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ReportLoader: React.FC = () => {
|
const ReportLoader: React.FC = () => {
|
||||||
const [report, setReport] = React.useState<LoadedReport | undefined>();
|
const [report, setReport] = React.useState<LoadedReport | undefined>();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -96,14 +43,13 @@ window.onload = () => {
|
||||||
|
|
||||||
class ZipReport implements LoadedReport {
|
class ZipReport implements LoadedReport {
|
||||||
private _entries = new Map<string, zip.Entry>();
|
private _entries = new Map<string, zip.Entry>();
|
||||||
private _json!: HTMLReport & { metadata?: Metadata };
|
private _json!: HTMLReport;
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader((window as any).playwrightReportBase64), { useWebWorkers: false }) as zip.ZipReader;
|
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader((window as any).playwrightReportBase64), { useWebWorkers: false }) as zip.ZipReader;
|
||||||
for (const entry of await zipReader.getEntries())
|
for (const entry of await zipReader.getEntries())
|
||||||
this._entries.set(entry.filename, entry);
|
this._entries.set(entry.filename, entry);
|
||||||
this._json = await this.entry('report.json') as HTMLReport;
|
this._json = await this.entry('report.json') as HTMLReport;
|
||||||
this._json.metadata = extractMetadata(this._json.attachments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
json(): HTMLReport {
|
json(): HTMLReport {
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { HTMLReport } from '@playwright-test/reporters/html';
|
import type { HTMLReport } from '@playwright-test/reporters/html';
|
||||||
import type { Metadata } from './index';
|
|
||||||
|
|
||||||
export interface LoadedReport {
|
export interface LoadedReport {
|
||||||
json(): HTMLReport & { metadata?: Metadata };
|
json(): HTMLReport;
|
||||||
entry(name: string): Promise<Object | undefined>;
|
entry(name: string): Promise<Object | undefined>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,61 @@ import * as React from 'react';
|
||||||
import './colors.css';
|
import './colors.css';
|
||||||
import './common.css';
|
import './common.css';
|
||||||
import * as icons from './icons';
|
import * as icons from './icons';
|
||||||
import type { Metadata } from './index';
|
|
||||||
import { AutoChip } from './chip';
|
import { AutoChip } from './chip';
|
||||||
import './reportView.css';
|
import './reportView.css';
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
|
|
||||||
export const MetadataView: React.FC<Metadata> = metadata => {
|
export type Metainfo = {
|
||||||
|
'revision.id'?: string;
|
||||||
|
'revision.author'?: string;
|
||||||
|
'revision.email'?: string;
|
||||||
|
'revision.subject'?: string;
|
||||||
|
'revision.timestamp'?: number | Date;
|
||||||
|
'revision.link'?: string;
|
||||||
|
'ci.link'?: string;
|
||||||
|
'timestamp'?: number
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<{}, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||||
|
state: { error: Error | null, errorInfo: React.ErrorInfo | null } = {
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
this.setState({ error, errorInfo });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.error || this.state.errorInfo) {
|
||||||
|
return (
|
||||||
|
<AutoChip header={'Commit Metainfo Error'} dataTestId='metadata-error'>
|
||||||
|
<p>An error was encountered when trying to render Commit Metainfo. Please file a GitHub issue to report this error.</p>
|
||||||
|
<p>
|
||||||
|
<pre style={{ overflow: 'scroll' }}>{this.state.error?.message}<br/>{this.state.error?.stack}<br/>{this.state.errorInfo?.componentStack}</pre>
|
||||||
|
</p>
|
||||||
|
</AutoChip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MetadataView: React.FC<Metainfo> = metadata => <ErrorBoundary><InnerMetadataView {...metadata} /></ErrorBoundary>;
|
||||||
|
|
||||||
|
const InnerMetadataView: React.FC<Metainfo> = metadata => {
|
||||||
|
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoChip header={
|
<AutoChip header={
|
||||||
<span>
|
<span>
|
||||||
{metadata['revision.id'] && <span style={{ float: 'right', fontFamily: 'var(--monospace-font)' }}>
|
{metadata['revision.id'] && <span style={{ float: 'right', fontFamily: 'var(--monospace-font)' }}>
|
||||||
{metadata['revision.id'].slice(0, 7)}
|
{metadata['revision.id'].slice(0, 7)}
|
||||||
</span>}
|
</span>}
|
||||||
{metadata['revision.subject'] && metadata['revision.subject'] || 'no subject>'}
|
{metadata['revision.subject'] || 'Commit Metainfo'}
|
||||||
{!metadata['revision.subject'] && 'Commit metainfo'}
|
</span>} initialExpanded={false} dataTestId='metadata-chip'>
|
||||||
</span>} initialExpanded={false}>
|
|
||||||
{metadata['revision.subject'] &&
|
{metadata['revision.subject'] &&
|
||||||
<MetadatViewItem
|
<MetadatViewItem
|
||||||
testId='revision.subject'
|
testId='revision.subject'
|
||||||
|
|
@ -73,10 +113,10 @@ export const MetadataView: React.FC<Metadata> = metadata => {
|
||||||
icon='externalLink'
|
icon='externalLink'
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{metadata['generatedAt'] &&
|
{metadata['timestamp'] &&
|
||||||
<MetadatViewItem
|
<MetadatViewItem
|
||||||
content={<span style={{ color: 'var(--color-fg-subtle)' }}>
|
content={<span style={{ color: 'var(--color-fg-subtle)' }}>
|
||||||
Report generated on {Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(metadata['generatedAt'])}
|
Report generated on {Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(metadata['timestamp'])}
|
||||||
</span>}></MetadatViewItem>
|
</span>}></MetadatViewItem>
|
||||||
}
|
}
|
||||||
</AutoChip>
|
</AutoChip>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { HeaderView } from './headerView';
|
||||||
import { Route } from './links';
|
import { Route } 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 { MetadataView } from './metadataView';
|
||||||
import { TestCaseView } from './testCaseView';
|
import { TestCaseView } from './testCaseView';
|
||||||
import { TestFilesView } from './testFilesView';
|
import { TestFilesView } from './testFilesView';
|
||||||
|
|
@ -46,7 +47,7 @@ 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!} />}
|
{report?.json().metadata && <MetadataView {...report?.json().metadata as Metainfo} />}
|
||||||
<Route params=''>
|
<Route params=''>
|
||||||
<TestFilesView report={report?.json()} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles}></TestFilesView>
|
<TestFilesView report={report?.json()} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles}></TestFilesView>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
import { createGuid } from 'playwright-core/lib/utils';
|
|
||||||
import { spawnAsync } from 'playwright-core/lib/utils/spawnAsync';
|
|
||||||
|
|
||||||
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
|
||||||
const kContentTypePlainText = 'text/plain';
|
|
||||||
const kContentTypeJSON = 'application/json';
|
|
||||||
export interface Attachment {
|
|
||||||
name: string;
|
|
||||||
contentType: string;
|
|
||||||
path?: string;
|
|
||||||
body?: Buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const gitStatusFromCLI = async (gitDir: string): Promise<Attachment[]> => {
|
|
||||||
const separator = `:${createGuid().slice(0, 4)}:`;
|
|
||||||
const { code, stdout } = 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 }
|
|
||||||
);
|
|
||||||
if (code)
|
|
||||||
return [];
|
|
||||||
const showOutput = stdout.trim();
|
|
||||||
const [sha, subject, authorName, authorEmail, rawTimestamp] = showOutput.split(separator);
|
|
||||||
let timestamp: number = Number.parseInt(rawTimestamp, 10);
|
|
||||||
timestamp = Number.isInteger(timestamp) ? timestamp * 1000 : 0;
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ name: 'revision.id', body: Buffer.from(sha), contentType: kContentTypePlainText },
|
|
||||||
{ name: 'revision.author', body: Buffer.from(authorName), contentType: kContentTypePlainText },
|
|
||||||
{ name: 'revision.email', body: Buffer.from(authorEmail), contentType: kContentTypePlainText },
|
|
||||||
{ name: 'revision.subject', body: Buffer.from(subject), contentType: kContentTypePlainText },
|
|
||||||
{ name: 'revision.timestamp', body: Buffer.from(JSON.stringify(timestamp)), contentType: kContentTypeJSON },
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const githubEnv = async (): Promise<Attachment[]> => {
|
|
||||||
const out: Attachment[] = [];
|
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_SHA)
|
|
||||||
out.push({ name: 'revision.link', body: Buffer.from(`${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`), contentType: kContentTypePlainText });
|
|
||||||
|
|
||||||
if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID)
|
|
||||||
out.push({ name: 'ci.link', body: Buffer.from(`${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`), contentType: kContentTypePlainText });
|
|
||||||
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const gitlabEnv = async (): Promise<Attachment[]> => {
|
|
||||||
// GitLab: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
|
|
||||||
const out: Attachment[] = [];
|
|
||||||
if (process.env.CI_PROJECT_URL && process.env.CI_COMMIT_SHA)
|
|
||||||
out.push({ name: 'revision.link', body: Buffer.from(`${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`), contentType: kContentTypePlainText });
|
|
||||||
|
|
||||||
if (process.env.CI_JOB_URL)
|
|
||||||
out.push({ name: 'ci.link', body: Buffer.from(process.env.CI_JOB_URL), contentType: kContentTypePlainText });
|
|
||||||
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const jenkinsEnv = async (): Promise<Attachment[]> => {
|
|
||||||
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
|
||||||
const out: Attachment[] = [];
|
|
||||||
if (process.env.BUILD_URL)
|
|
||||||
out.push({ name: 'ci.link', body: Buffer.from(process.env.BUILD_URL), contentType: kContentTypePlainText });
|
|
||||||
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generationTimestamp = async (): Promise<Attachment[]> => {
|
|
||||||
return [{ name: 'generatedAt', body: Buffer.from(JSON.stringify(Date.now())), contentType: kContentTypeJSON }];
|
|
||||||
};
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
import type { FullConfigInternal, GlobalInfo } from './types';
|
|
||||||
import { normalizeAndSaveAttachment } from './util';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
export class GlobalInfoImpl implements GlobalInfo {
|
|
||||||
private _fullConfig: FullConfigInternal;
|
|
||||||
private _attachments: { name: string; path?: string | undefined; body?: Buffer | undefined; contentType: string; }[] = [];
|
|
||||||
|
|
||||||
constructor(config: FullConfigInternal) {
|
|
||||||
this._fullConfig = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
attachments() {
|
|
||||||
return [...this._attachments];
|
|
||||||
}
|
|
||||||
|
|
||||||
async attach(name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}) {
|
|
||||||
await fs.promises.mkdir(this._fullConfig._globalOutputDir, { recursive: true });
|
|
||||||
this._attachments.push(await normalizeAndSaveAttachment(this._fullConfig._globalOutputDir, name, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { installTransform, setCurrentlyLoadingTestFile } from './transform';
|
import { installTransform, setCurrentlyLoadingTestFile } from './transform';
|
||||||
import type { Config, Project, ReporterDescription, FullProjectInternal, GlobalInfo } from './types';
|
import type { Config, Project, ReporterDescription, FullProjectInternal } from './types';
|
||||||
import type { FullConfigInternal } from './types';
|
import type { FullConfigInternal } from './types';
|
||||||
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
|
import { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
|
|
@ -163,6 +163,7 @@ export class Loader {
|
||||||
this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
|
this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
|
||||||
this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
|
this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
|
||||||
this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins);
|
this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins);
|
||||||
|
this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
|
||||||
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath));
|
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,7 +211,7 @@ export class Loader {
|
||||||
return suite;
|
return suite;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadGlobalHook(file: string, name: string): Promise<(config: FullConfigInternal, globalInfo?: GlobalInfo) => any> {
|
async loadGlobalHook(file: string, name: string): Promise<(config: FullConfigInternal) => any> {
|
||||||
let hook = await this._requireOrImport(file);
|
let hook = await this._requireOrImport(file);
|
||||||
if (hook && typeof hook === 'object' && ('default' in hook))
|
if (hook && typeof hook === 'object' && ('default' in hook))
|
||||||
hook = hook['default'];
|
hook = hook['default'];
|
||||||
|
|
@ -516,6 +517,7 @@ export const baseFullConfig: FullConfigInternal = {
|
||||||
grep: /.*/,
|
grep: /.*/,
|
||||||
grepInvert: null,
|
grepInvert: null,
|
||||||
maxFailures: 0,
|
maxFailures: 0,
|
||||||
|
metadata: {},
|
||||||
preserveOutput: 'always',
|
preserveOutput: 'always',
|
||||||
projects: [],
|
projects: [],
|
||||||
reporter: [ [process.env.CI ? 'dot' : 'list'] ],
|
reporter: [ [process.env.CI ? 'dot' : 'list'] ],
|
||||||
|
|
|
||||||
97
packages/playwright-test/src/plugins/gitCommitInfoPlugin.ts
Normal file
97
packages/playwright-test/src/plugins/gitCommitInfoPlugin.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import type { Config, TestPlugin } from '../types';
|
||||||
|
import { createGuid } from 'playwright-core/lib/utils';
|
||||||
|
import { spawnAsync } from 'playwright-core/lib/utils/spawnAsync';
|
||||||
|
|
||||||
|
const GIT_OPERATIONS_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestPlugin => {
|
||||||
|
return {
|
||||||
|
name: 'playwright:git-commit-info',
|
||||||
|
|
||||||
|
configure: async (config: Config, configDir: string) => {
|
||||||
|
const info = {
|
||||||
|
...linksFromEnv(),
|
||||||
|
...options?.info ? options.info : await gitStatusFromCLI(options?.directory || configDir),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
// Normalize dates
|
||||||
|
const timestamp = info['revision.timestamp'];
|
||||||
|
if (timestamp instanceof Date)
|
||||||
|
info['revision.timestamp'] = timestamp.getTime();
|
||||||
|
|
||||||
|
config.metadata = config.metadata || {};
|
||||||
|
Object.assign(config.metadata, info);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GitCommitInfoPluginOptions {
|
||||||
|
directory?: string;
|
||||||
|
info?: Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
'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; } = {};
|
||||||
|
// Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
|
||||||
|
if (process.env.BUILD_URL)
|
||||||
|
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.link'] = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`;
|
||||||
|
if (process.env.CI_JOB_URL)
|
||||||
|
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.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.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gitStatusFromCLI = async (gitDir: string): Promise<Info | undefined> => {
|
||||||
|
const separator = `:${createGuid().slice(0, 4)}:`;
|
||||||
|
const { code, stdout } = 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 }
|
||||||
|
);
|
||||||
|
if (code)
|
||||||
|
return;
|
||||||
|
const showOutput = 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;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'revision.id': id,
|
||||||
|
'revision.author': author,
|
||||||
|
'revision.email': email,
|
||||||
|
'revision.subject': subject,
|
||||||
|
'revision.timestamp': timestamp,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -14,3 +14,4 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
export { webServer } from './webServerPlugin';
|
export { webServer } from './webServerPlugin';
|
||||||
|
export { gitCommitInfo } from './gitCommitInfoPlugin';
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResul
|
||||||
import RawReporter from './raw';
|
import RawReporter from './raw';
|
||||||
import { stripAnsiEscapes } from './base';
|
import { stripAnsiEscapes } from './base';
|
||||||
import { getPackageJsonPath } from '../util';
|
import { getPackageJsonPath } from '../util';
|
||||||
import type { FullConfigInternal } from '../types';
|
import type { FullConfigInternal, Metadata } from '../types';
|
||||||
import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
import type { ZipFile } from 'playwright-core/lib/zipBundle';
|
||||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ export type Location = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HTMLReport = {
|
export type HTMLReport = {
|
||||||
attachments: TestAttachment[];
|
metadata: Metadata;
|
||||||
files: TestFileSummary[];
|
files: TestFileSummary[];
|
||||||
stats: Stats;
|
stats: Stats;
|
||||||
projectNames: string[];
|
projectNames: string[];
|
||||||
|
|
@ -159,12 +159,12 @@ class HtmlReporter implements Reporter {
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
const reports = projectSuites.map(suite => {
|
const reports = projectSuites.map(suite => {
|
||||||
const rawReporter = new RawReporter();
|
const rawReporter = new RawReporter();
|
||||||
const report = rawReporter.generateProjectReport(this.config, suite, []);
|
const report = rawReporter.generateProjectReport(this.config, suite);
|
||||||
return report;
|
return report;
|
||||||
});
|
});
|
||||||
await removeFolders([outputFolder]);
|
await removeFolders([outputFolder]);
|
||||||
const builder = new HtmlBuilder(outputFolder);
|
const builder = new HtmlBuilder(outputFolder);
|
||||||
const { ok, singleTestId } = await builder.build(new RawReporter().generateAttachments(this.suite.attachments), reports);
|
const { ok, singleTestId } = await builder.build(this.config.metadata, reports);
|
||||||
|
|
||||||
if (process.env.CI)
|
if (process.env.CI)
|
||||||
return;
|
return;
|
||||||
|
|
@ -255,7 +255,7 @@ class HtmlBuilder {
|
||||||
this._dataZipFile = new yazl.ZipFile();
|
this._dataZipFile = new yazl.ZipFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(testReportAttachments: JsonAttachment[], rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
|
async build(metadata: Metadata, rawReports: JsonReport[]): Promise<{ ok: boolean, singleTestId: string | undefined }> {
|
||||||
|
|
||||||
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
const data = new Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>();
|
||||||
for (const projectJson of rawReports) {
|
for (const projectJson of rawReports) {
|
||||||
|
|
@ -311,7 +311,7 @@ class HtmlBuilder {
|
||||||
this._addDataFile(fileId + '.json', testFile);
|
this._addDataFile(fileId + '.json', testFile);
|
||||||
}
|
}
|
||||||
const htmlReport: HTMLReport = {
|
const htmlReport: HTMLReport = {
|
||||||
attachments: this._serializeAttachments(testReportAttachments),
|
metadata,
|
||||||
files: [...data.values()].map(e => e.testFileSummary),
|
files: [...data.values()].map(e => e.testFileSummary),
|
||||||
projectNames: rawReports.map(r => r.project.name),
|
projectNames: rawReports.map(r => r.project.name),
|
||||||
stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
|
stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { formatResultFailure } from './base';
|
||||||
import { toPosixPath, serializePatterns } from './json';
|
import { toPosixPath, serializePatterns } from './json';
|
||||||
import { MultiMap } from 'playwright-core/lib/utils/multimap';
|
import { MultiMap } from 'playwright-core/lib/utils/multimap';
|
||||||
import { codeFrameColumns } from '../babelBundle';
|
import { codeFrameColumns } from '../babelBundle';
|
||||||
|
import type { Metadata } from '../types';
|
||||||
|
|
||||||
export type JsonLocation = Location;
|
export type JsonLocation = Location;
|
||||||
export type JsonError = string;
|
export type JsonError = string;
|
||||||
|
|
@ -30,7 +31,6 @@ export type JsonStackFrame = { file: string, line: number, column: number };
|
||||||
|
|
||||||
export type JsonReport = {
|
export type JsonReport = {
|
||||||
config: JsonConfig,
|
config: JsonConfig,
|
||||||
attachments: JsonAttachment[],
|
|
||||||
project: JsonProject,
|
project: JsonProject,
|
||||||
suites: JsonSuite[],
|
suites: JsonSuite[],
|
||||||
};
|
};
|
||||||
|
|
@ -38,7 +38,7 @@ export type JsonReport = {
|
||||||
export type JsonConfig = Omit<FullConfig, 'projects' | 'attachments'>;
|
export type JsonConfig = Omit<FullConfig, 'projects' | 'attachments'>;
|
||||||
|
|
||||||
export type JsonProject = {
|
export type JsonProject = {
|
||||||
metadata: any,
|
metadata: Metadata,
|
||||||
name: string,
|
name: string,
|
||||||
outputDir: string,
|
outputDir: string,
|
||||||
repeatEach: number,
|
repeatEach: number,
|
||||||
|
|
@ -112,7 +112,6 @@ class RawReporter {
|
||||||
|
|
||||||
async onEnd() {
|
async onEnd() {
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
const globalAttachments = this.generateAttachments(this.suite.attachments);
|
|
||||||
for (const suite of projectSuites) {
|
for (const suite of projectSuites) {
|
||||||
const project = suite.project();
|
const project = suite.project();
|
||||||
assert(project, 'Internal Error: Invalid project structure');
|
assert(project, 'Internal Error: Invalid project structure');
|
||||||
|
|
@ -130,7 +129,7 @@ class RawReporter {
|
||||||
}
|
}
|
||||||
if (!reportFile)
|
if (!reportFile)
|
||||||
throw new Error('Internal error, could not create report file');
|
throw new Error('Internal error, could not create report file');
|
||||||
const report = this.generateProjectReport(this.config, suite, globalAttachments);
|
const report = this.generateProjectReport(this.config, suite);
|
||||||
fs.writeFileSync(reportFile, JSON.stringify(report, undefined, 2));
|
fs.writeFileSync(reportFile, JSON.stringify(report, undefined, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,13 +162,12 @@ class RawReporter {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateProjectReport(config: FullConfig, suite: Suite, attachments: JsonAttachment[]): JsonReport {
|
generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
const project = suite.project();
|
const project = suite.project();
|
||||||
assert(project, 'Internal Error: Invalid project structure');
|
assert(project, 'Internal Error: Invalid project structure');
|
||||||
const report: JsonReport = {
|
const report: JsonReport = {
|
||||||
config,
|
config,
|
||||||
attachments,
|
|
||||||
project: {
|
project: {
|
||||||
metadata: project.metadata,
|
metadata: project.metadata,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ import type { Config, FullProjectInternal } from './types';
|
||||||
import type { FullConfigInternal } from './types';
|
import type { FullConfigInternal } from './types';
|
||||||
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
|
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
|
||||||
import { SigIntWatcher } from './sigIntWatcher';
|
import { SigIntWatcher } from './sigIntWatcher';
|
||||||
import { GlobalInfoImpl } from './globalInfo';
|
|
||||||
|
|
||||||
const removeFolderAsync = promisify(rimraf);
|
const removeFolderAsync = promisify(rimraf);
|
||||||
const readDirAsync = promisify(fs.readdir);
|
const readDirAsync = promisify(fs.readdir);
|
||||||
|
|
@ -78,11 +77,9 @@ export type ConfigCLIOverrides = {
|
||||||
export class Runner {
|
export class Runner {
|
||||||
private _loader: Loader;
|
private _loader: Loader;
|
||||||
private _reporter!: Reporter;
|
private _reporter!: Reporter;
|
||||||
private _globalInfo: GlobalInfoImpl;
|
|
||||||
|
|
||||||
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
constructor(configCLIOverrides?: ConfigCLIOverrides) {
|
||||||
this._loader = new Loader(configCLIOverrides);
|
this._loader = new Loader(configCLIOverrides);
|
||||||
this._globalInfo = new GlobalInfoImpl(this._loader.fullConfig());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
|
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise<FullConfigInternal> {
|
||||||
|
|
@ -398,9 +395,6 @@ export class Runner {
|
||||||
|
|
||||||
const result: FullResult = { status: 'passed' };
|
const result: FullResult = { status: 'passed' };
|
||||||
|
|
||||||
// 13.5 Add copy of attachments.
|
|
||||||
rootSuite.attachments = this._globalInfo.attachments();
|
|
||||||
|
|
||||||
// 14. Run tests.
|
// 14. Run tests.
|
||||||
try {
|
try {
|
||||||
const sigintWatcher = new SigIntWatcher();
|
const sigintWatcher = new SigIntWatcher();
|
||||||
|
|
@ -466,7 +460,7 @@ export class Runner {
|
||||||
|
|
||||||
// The do global setup.
|
// The do global setup.
|
||||||
if (config.globalSetup)
|
if (config.globalSetup)
|
||||||
globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig(), this._globalInfo);
|
globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig());
|
||||||
}, result);
|
}, result);
|
||||||
|
|
||||||
if (result.status !== 'passed') {
|
if (result.status !== 'passed') {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ export type Modifier = {
|
||||||
export class Suite extends Base implements reporterTypes.Suite {
|
export class Suite extends Base implements reporterTypes.Suite {
|
||||||
suites: Suite[] = [];
|
suites: Suite[] = [];
|
||||||
tests: TestCase[] = [];
|
tests: TestCase[] = [];
|
||||||
attachments: reporterTypes.Suite['attachments'] = [];
|
|
||||||
location?: Location;
|
location?: Location;
|
||||||
parent?: Suite;
|
parent?: Suite;
|
||||||
_use: FixturesWithLocation[] = [];
|
_use: FixturesWithLocation[] = [];
|
||||||
|
|
|
||||||
126
packages/playwright-test/types/test.d.ts
vendored
126
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -628,9 +628,9 @@ interface TestConfig {
|
||||||
maxFailures?: number;
|
maxFailures?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any JSON-serializable metadata that will be put directly to the test report.
|
* Metadata that will be put directly to the test report serialized as JSON.
|
||||||
*/
|
*/
|
||||||
metadata?: any;
|
metadata?: Metadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config name is visible in the report and during test execution, unless overridden by
|
* Config name is visible in the report and during test execution, unless overridden by
|
||||||
|
|
@ -921,6 +921,8 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
|
||||||
use?: UseOptions<TestArgs, WorkerArgs>;
|
use?: UseOptions<TestArgs, WorkerArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Metadata = { [key: string]: string | number | boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
|
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
|
||||||
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
|
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
|
||||||
|
|
@ -1055,6 +1057,10 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
maxFailures: number;
|
maxFailures: number;
|
||||||
|
/**
|
||||||
|
* Metadata that will be put directly to the test report serialized as JSON.
|
||||||
|
*/
|
||||||
|
metadata: Metadata;
|
||||||
version: string;
|
version: string;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve test output in the
|
* Whether to preserve test output in the
|
||||||
|
|
@ -3501,122 +3507,6 @@ interface ScreenshotAssertions {
|
||||||
}): void;
|
}): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* `GlobalInfo` contains information on the overall test run. The information spans projects and tests. Some reporters show
|
|
||||||
* global info.
|
|
||||||
*
|
|
||||||
* You can write to GlobalInfo via your Global Setup hook, and read from it in a [Custom Reporter](https://playwright.dev/docs/test-reporters):
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // global-setup.ts
|
|
||||||
* import { chromium, FullConfig, GlobalInfo } from '@playwright/test';
|
|
||||||
*
|
|
||||||
* async function globalSetup(config: FullConfig, info: GlobalInfo) {
|
|
||||||
* await info.attach('agent.config.txt', { path: './agent.config.txt' });
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* export default globalSetup;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Access the attachments from the Root Suite in the Reporter:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // my-awesome-reporter.ts
|
|
||||||
* import { Reporter } from '@playwright/test/reporter';
|
|
||||||
*
|
|
||||||
* class MyReporter implements Reporter {
|
|
||||||
* private _suite;
|
|
||||||
*
|
|
||||||
* onBegin(config, suite) {
|
|
||||||
* this._suite = suite;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* onEnd(result) {
|
|
||||||
* console.log(`Finished the run with ${this._suite.attachments.length} global attachments!`);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* export default MyReporter;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Finally, specify `globalSetup` in the configuration file and `reporter`:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // playwright.config.ts
|
|
||||||
* import { PlaywrightTestConfig } from '@playwright/test';
|
|
||||||
*
|
|
||||||
* const config: PlaywrightTestConfig = {
|
|
||||||
* globalSetup: require.resolve('./global-setup'),
|
|
||||||
* reporter: require.resolve('./my-awesome-reporter'),
|
|
||||||
* };
|
|
||||||
* export default config;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* See [`TestInfo`](https://playwright.dev/docs/api/class-testinfo) for related attachment functionality scoped to the test-level.
|
|
||||||
*/
|
|
||||||
export interface GlobalInfo {
|
|
||||||
/**
|
|
||||||
* The list of files or buffers attached to the overall test run. Some reporters show global attachments.
|
|
||||||
*
|
|
||||||
* To add an attachment, use
|
|
||||||
* [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach). See
|
|
||||||
* [testInfo.attachments](https://playwright.dev/docs/api/class-testinfo#test-info-attachments) if you are looking for
|
|
||||||
* test-scoped attachments.
|
|
||||||
*/
|
|
||||||
attachments(): Array<{
|
|
||||||
/**
|
|
||||||
* Attachment name.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
*/
|
|
||||||
contentType: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional path on the filesystem to the attached file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional attachment body used instead of a file.
|
|
||||||
*/
|
|
||||||
body?: Buffer;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach a value or a file from disk to the overall test run. Some reporters show global attachments. Either `path` or
|
|
||||||
* `body` must be specified, but not both.
|
|
||||||
*
|
|
||||||
* See [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach) if you are
|
|
||||||
* looking for test-scoped attachments.
|
|
||||||
*
|
|
||||||
* > NOTE: [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach)
|
|
||||||
* automatically takes care of copying attached files to a location that is accessible to reporters. You can safely remove
|
|
||||||
* the attachment after awaiting the attach call.
|
|
||||||
* @param name Attachment name.
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
attach(name: string, options?: {
|
|
||||||
/**
|
|
||||||
* Attachment body. Mutually exclusive with `path`.
|
|
||||||
*/
|
|
||||||
body?: string|Buffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional content type of this attachment to properly present in the report, for example `'application/json'` or
|
|
||||||
* `'image/png'`. If omitted, content type is inferred based on the `path`, or defaults to `text/plain` for [string]
|
|
||||||
* attachments and `application/octet-stream` for [Buffer] attachments.
|
|
||||||
*/
|
|
||||||
contentType?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path on the filesystem to the attached file. Mutually exclusive with `body`.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
}): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
28
packages/playwright-test/types/testReporter.d.ts
vendored
28
packages/playwright-test/types/testReporter.d.ts
vendored
|
|
@ -84,33 +84,7 @@ export interface Suite {
|
||||||
/**
|
/**
|
||||||
* Returns a list of titles from the root down to this suite.
|
* Returns a list of titles from the root down to this suite.
|
||||||
*/
|
*/
|
||||||
titlePath(): Array<string>;
|
titlePath(): Array<string>;}
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of files or buffers attached to the suite. Root suite has attachments populated by
|
|
||||||
* [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach).
|
|
||||||
*/
|
|
||||||
attachments: Array<{
|
|
||||||
/**
|
|
||||||
* Attachment name.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
*/
|
|
||||||
contentType: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional path on the filesystem to the attached file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional attachment body used instead of a file.
|
|
||||||
*/
|
|
||||||
body?: Buffer;
|
|
||||||
}>;}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,16 @@ loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
|
||||||
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
|
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { ServerWorkerOptions } from '../config/serverFixtures';
|
import type { ServerWorkerOptions } from '../config/serverFixtures';
|
||||||
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
|
|
||||||
process.env.PWPAGE_IMPL = 'android';
|
process.env.PWPAGE_IMPL = 'android';
|
||||||
|
|
||||||
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||||
const testDir = path.join(__dirname, '..');
|
const testDir = path.join(__dirname, '..');
|
||||||
const config: Config<ServerWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
const config: Config<ServerWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
||||||
globalSetup: path.join(__dirname, '../config/globalSetup'),
|
plugins: [
|
||||||
|
gitCommitInfo(),
|
||||||
|
],
|
||||||
testDir,
|
testDir,
|
||||||
outputDir,
|
outputDir,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
|
|
|
||||||
154
tests/config/experimental.d.ts
vendored
154
tests/config/experimental.d.ts
vendored
|
|
@ -16865,9 +16865,9 @@ interface TestConfig {
|
||||||
maxFailures?: number;
|
maxFailures?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any JSON-serializable metadata that will be put directly to the test report.
|
* Metadata that will be put directly to the test report serialized as JSON.
|
||||||
*/
|
*/
|
||||||
metadata?: any;
|
metadata?: Metadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config name is visible in the report and during test execution, unless overridden by
|
* Config name is visible in the report and during test execution, unless overridden by
|
||||||
|
|
@ -17196,6 +17196,8 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
|
||||||
use?: UseOptions<TestArgs, WorkerArgs>;
|
use?: UseOptions<TestArgs, WorkerArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Metadata = { [key: string]: string | number | boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
|
* Playwright Test provides many options to configure how your tests are collected and executed, for example `timeout` or
|
||||||
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
|
* `testDir`. These options are described in the [TestConfig] object in the [configuration file](https://playwright.dev/docs/test-configuration).
|
||||||
|
|
@ -17330,6 +17332,10 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
maxFailures: number;
|
maxFailures: number;
|
||||||
|
/**
|
||||||
|
* Metadata that will be put directly to the test report serialized as JSON.
|
||||||
|
*/
|
||||||
|
metadata: Metadata;
|
||||||
version: string;
|
version: string;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve test output in the
|
* Whether to preserve test output in the
|
||||||
|
|
@ -19962,122 +19968,6 @@ interface ScreenshotAssertions {
|
||||||
}): void;
|
}): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* `GlobalInfo` contains information on the overall test run. The information spans projects and tests. Some reporters show
|
|
||||||
* global info.
|
|
||||||
*
|
|
||||||
* You can write to GlobalInfo via your Global Setup hook, and read from it in a [Custom Reporter](https://playwright.dev/docs/test-reporters):
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // global-setup.ts
|
|
||||||
* import { chromium, FullConfig, GlobalInfo } from '@playwright/test';
|
|
||||||
*
|
|
||||||
* async function globalSetup(config: FullConfig, info: GlobalInfo) {
|
|
||||||
* await info.attach('agent.config.txt', { path: './agent.config.txt' });
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* export default globalSetup;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Access the attachments from the Root Suite in the Reporter:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // my-awesome-reporter.ts
|
|
||||||
* import { Reporter } from '@playwright/test/reporter';
|
|
||||||
*
|
|
||||||
* class MyReporter implements Reporter {
|
|
||||||
* private _suite;
|
|
||||||
*
|
|
||||||
* onBegin(config, suite) {
|
|
||||||
* this._suite = suite;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* onEnd(result) {
|
|
||||||
* console.log(`Finished the run with ${this._suite.attachments.length} global attachments!`);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* export default MyReporter;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Finally, specify `globalSetup` in the configuration file and `reporter`:
|
|
||||||
*
|
|
||||||
* ```ts
|
|
||||||
* // playwright.config.ts
|
|
||||||
* import { PlaywrightTestConfig } from '@playwright/test';
|
|
||||||
*
|
|
||||||
* const config: PlaywrightTestConfig = {
|
|
||||||
* globalSetup: require.resolve('./global-setup'),
|
|
||||||
* reporter: require.resolve('./my-awesome-reporter'),
|
|
||||||
* };
|
|
||||||
* export default config;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* See [`TestInfo`](https://playwright.dev/docs/api/class-testinfo) for related attachment functionality scoped to the test-level.
|
|
||||||
*/
|
|
||||||
export interface GlobalInfo {
|
|
||||||
/**
|
|
||||||
* The list of files or buffers attached to the overall test run. Some reporters show global attachments.
|
|
||||||
*
|
|
||||||
* To add an attachment, use
|
|
||||||
* [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach). See
|
|
||||||
* [testInfo.attachments](https://playwright.dev/docs/api/class-testinfo#test-info-attachments) if you are looking for
|
|
||||||
* test-scoped attachments.
|
|
||||||
*/
|
|
||||||
attachments(): Array<{
|
|
||||||
/**
|
|
||||||
* Attachment name.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
*/
|
|
||||||
contentType: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional path on the filesystem to the attached file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional attachment body used instead of a file.
|
|
||||||
*/
|
|
||||||
body?: Buffer;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach a value or a file from disk to the overall test run. Some reporters show global attachments. Either `path` or
|
|
||||||
* `body` must be specified, but not both.
|
|
||||||
*
|
|
||||||
* See [testInfo.attach(name[, options])](https://playwright.dev/docs/api/class-testinfo#test-info-attach) if you are
|
|
||||||
* looking for test-scoped attachments.
|
|
||||||
*
|
|
||||||
* > NOTE: [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach)
|
|
||||||
* automatically takes care of copying attached files to a location that is accessible to reporters. You can safely remove
|
|
||||||
* the attachment after awaiting the attach call.
|
|
||||||
* @param name Attachment name.
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
attach(name: string, options?: {
|
|
||||||
/**
|
|
||||||
* Attachment body. Mutually exclusive with `path`.
|
|
||||||
*/
|
|
||||||
body?: string|Buffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional content type of this attachment to properly present in the report, for example `'application/json'` or
|
|
||||||
* `'image/png'`. If omitted, content type is inferred based on the `path`, or defaults to `text/plain` for [string]
|
|
||||||
* attachments and `application/octet-stream` for [Buffer] attachments.
|
|
||||||
*/
|
|
||||||
contentType?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Path on the filesystem to the attached file. Mutually exclusive with `body`.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
}): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
|
|
@ -20609,33 +20499,7 @@ export interface Suite {
|
||||||
/**
|
/**
|
||||||
* Returns a list of titles from the root down to this suite.
|
* Returns a list of titles from the root down to this suite.
|
||||||
*/
|
*/
|
||||||
titlePath(): Array<string>;
|
titlePath(): Array<string>;}
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of files or buffers attached to the suite. Root suite has attachments populated by
|
|
||||||
* [globalInfo.attach(name[, options])](https://playwright.dev/docs/api/class-globalinfo#global-info-attach).
|
|
||||||
*/
|
|
||||||
attachments: Array<{
|
|
||||||
/**
|
|
||||||
* Attachment name.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
|
||||||
*/
|
|
||||||
contentType: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional path on the filesystem to the attached file.
|
|
||||||
*/
|
|
||||||
path?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional attachment body used instead of a file.
|
|
||||||
*/
|
|
||||||
body?: Buffer;
|
|
||||||
}>;}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
import type { FullConfig, GlobalInfo } from '@playwright/test';
|
|
||||||
|
|
||||||
// We're dogfooding this, so the …/lib/… import is acceptable
|
|
||||||
import * as ci from '@playwright/test/lib/ci';
|
|
||||||
|
|
||||||
async function globalSetup(config: FullConfig, globalInfo: GlobalInfo) {
|
|
||||||
const pluginResults = await Promise.all([
|
|
||||||
ci.generationTimestamp(),
|
|
||||||
ci.gitStatusFromCLI(config.rootDir),
|
|
||||||
ci.githubEnv(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all(pluginResults.flat().map(attachment => globalInfo.attach(attachment.name, attachment)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default globalSetup;
|
|
||||||
|
|
@ -20,13 +20,16 @@ loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
|
||||||
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
|
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
|
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
|
||||||
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
|
|
||||||
process.env.PWPAGE_IMPL = 'electron';
|
process.env.PWPAGE_IMPL = 'electron';
|
||||||
|
|
||||||
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||||
const testDir = path.join(__dirname, '..');
|
const testDir = path.join(__dirname, '..');
|
||||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions> = {
|
||||||
globalSetup: path.join(__dirname, '../config/globalSetup'),
|
plugins: [
|
||||||
|
gitCommitInfo(),
|
||||||
|
],
|
||||||
testDir,
|
testDir,
|
||||||
outputDir,
|
outputDir,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,13 @@ import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
import { config as loadEnv } from 'dotenv';
|
import { config as loadEnv } from 'dotenv';
|
||||||
loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
|
loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
|
||||||
|
|
||||||
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
|
plugins: [
|
||||||
|
gitCommitInfo(),
|
||||||
|
],
|
||||||
testIgnore: '**\/fixture-scripts/**',
|
testIgnore: '**\/fixture-scripts/**',
|
||||||
globalSetup: path.join(__dirname, 'globalSetup'),
|
|
||||||
timeout: 5 * 60 * 1000,
|
timeout: 5 * 60 * 1000,
|
||||||
retries: 0,
|
retries: 0,
|
||||||
reporter: process.env.CI ? 'dot' : [['list'], ['html', { open: 'on-failure' }]],
|
reporter: process.env.CI ? 'dot' : [['list'], ['html', { open: 'on-failure' }]],
|
||||||
|
|
|
||||||
|
|
@ -694,4 +694,3 @@ it('should not hang on resources served from cache', async ({ contextFactory, se
|
||||||
else
|
else
|
||||||
expect(entries.length).toBe(2);
|
expect(entries.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@pl
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { TestModeWorkerOptions } from '../config/testModeFixtures';
|
import type { TestModeWorkerOptions } from '../config/testModeFixtures';
|
||||||
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
|
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
|
||||||
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
|
|
||||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
||||||
|
|
@ -44,7 +45,9 @@ const trace = !!process.env.PWTEST_TRACE;
|
||||||
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
const outputDir = path.join(__dirname, '..', '..', 'test-results');
|
||||||
const testDir = path.join(__dirname, '..');
|
const testDir = path.join(__dirname, '..');
|
||||||
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
const config: Config<CoverageWorkerOptions & PlaywrightWorkerOptions & PlaywrightTestOptions & TestModeWorkerOptions> = {
|
||||||
globalSetup: path.join(__dirname, '../config/globalSetup'),
|
plugins: [
|
||||||
|
gitCommitInfo(),
|
||||||
|
],
|
||||||
testDir,
|
testDir,
|
||||||
outputDir,
|
outputDir,
|
||||||
expect: {
|
expect: {
|
||||||
|
|
|
||||||
|
|
@ -704,64 +704,152 @@ test('open tests from required file', async ({ runInlineTest, showReport, page }
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should include metadata', async ({ runInlineTest, showReport, page }) => {
|
test.describe('gitCommitInfo plugin', () => {
|
||||||
const beforeRunPlaywrightTest = async ({ baseDir }: { baseDir: string }) => {
|
test('should include metadata', async ({ runInlineTest, showReport, page }) => {
|
||||||
const execGit = async (args: string[]) => {
|
const beforeRunPlaywrightTest = async ({ baseDir }: { baseDir: string }) => {
|
||||||
const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
|
const execGit = async (args: string[]) => {
|
||||||
if (!!code)
|
const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
|
||||||
throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
|
if (!!code)
|
||||||
return;
|
throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
await execGit(['init']);
|
||||||
|
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
||||||
|
await execGit(['config', '--local', 'user.name', 'William']);
|
||||||
|
await execGit(['add', '*.ts']);
|
||||||
|
await execGit(['commit', '-m', 'awesome commit message']);
|
||||||
};
|
};
|
||||||
|
|
||||||
await execGit(['init']);
|
const result = await runInlineTest({
|
||||||
await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']);
|
'uncommitted.txt': `uncommitted file`,
|
||||||
await execGit(['config', '--local', 'user.name', 'William']);
|
'playwright.config.ts': `
|
||||||
await execGit(['add', '*.ts']);
|
import path from 'path';
|
||||||
await execGit(['commit', '-m', 'awesome commit message']);
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
};
|
|
||||||
|
|
||||||
const result = await runInlineTest({
|
const config = {
|
||||||
'uncommitted.txt': `uncommitted file`,
|
plugins: [ gitCommitInfo() ],
|
||||||
'globalSetup.ts': `
|
}
|
||||||
import { FullConfig, GlobalInfo } from '@playwright/test';
|
|
||||||
import * as ci from '@playwright/test/lib/ci';
|
|
||||||
|
|
||||||
async function globalSetup(config: FullConfig, globalInfo: GlobalInfo) {
|
export default config;
|
||||||
const pluginResults = await Promise.all([
|
`,
|
||||||
ci.generationTimestamp(),
|
'example.spec.ts': `
|
||||||
ci.gitStatusFromCLI(config.rootDir),
|
const { test } = pwt;
|
||||||
ci.githubEnv(),
|
test('sample', async ({}) => { expect(2).toBe(2); });
|
||||||
]);
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_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, beforeRunPlaywrightTest);
|
||||||
|
|
||||||
await Promise.all(pluginResults.flat().map(attachment => globalInfo.attach(attachment.name, attachment)));
|
await showReport();
|
||||||
}
|
|
||||||
|
|
||||||
export default globalSetup;
|
expect(result.exitCode).toBe(0);
|
||||||
`,
|
await page.click('text=awesome commit message');
|
||||||
'playwright.config.ts': `
|
await expect.soft(page.locator('data-test-id=revision.id')).toContainText(/^[a-f\d]+$/i);
|
||||||
import path from 'path';
|
await expect.soft(page.locator('data-test-id=revision.id >> a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
|
||||||
const config = {
|
await expect.soft(page.locator('data-test-id=revision.timestamp')).toContainText(/AM|PM/);
|
||||||
globalSetup: path.join(__dirname, './globalSetup'),
|
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.locator('data-test-id=metadata-chip')).toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=metadata-error')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
export default config;
|
|
||||||
`,
|
|
||||||
'example.spec.ts': `
|
|
||||||
const { test } = pwt;
|
|
||||||
test('sample', async ({}) => { expect(2).toBe(2); });
|
|
||||||
`,
|
|
||||||
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_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, beforeRunPlaywrightTest);
|
|
||||||
|
|
||||||
await showReport();
|
test('should use explicitly supplied metadata', async ({ runInlineTest, showReport, page }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'uncommitted.txt': `uncommitted file`,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import path from 'path';
|
||||||
|
import { gitCommitInfo } from '@playwright/test/lib/plugins';
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
const config = {
|
||||||
await page.click('text=awesome commit message');
|
plugins: [ gitCommitInfo({
|
||||||
await expect.soft(page.locator('data-test-id=revision.id')).toContainText(/^[a-f\d]+$/i);
|
info: {
|
||||||
await expect.soft(page.locator('data-test-id=revision.id >> a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
|
'revision.id': '1234567890',
|
||||||
await expect.soft(page.locator('data-test-id=revision.timestamp')).toContainText(/AM|PM/);
|
'revision.subject': 'a better subject',
|
||||||
await expect.soft(page.locator('text=awesome commit message')).toHaveCount(2);
|
'revision.timestamp': new Date(),
|
||||||
await expect.soft(page.locator('text=William')).toBeVisible();
|
'revision.author': 'William',
|
||||||
await expect.soft(page.locator('text=shakespeare@example.local')).toBeVisible();
|
'revision.email': 'shakespeare@example.local',
|
||||||
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/);
|
}) ],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
`,
|
||||||
|
'example.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('sample', async ({}) => { expect(2).toBe(2); });
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_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 page.click('text=a better subject');
|
||||||
|
await expect.soft(page.locator('data-test-id=revision.id')).toContainText(/^[a-f\d]+$/i);
|
||||||
|
await expect.soft(page.locator('data-test-id=revision.id >> a')).toHaveAttribute('href', 'https://playwright.dev/microsoft/playwright-example-for-test/commit/example-sha');
|
||||||
|
await expect.soft(page.locator('data-test-id=revision.timestamp')).toContainText(/AM|PM/);
|
||||||
|
await expect.soft(page.locator('text=a better subject')).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.locator('data-test-id=metadata-chip')).toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=metadata-error')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have metadata by default', async ({ runInlineTest, showReport, page }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'uncommitted.txt': `uncommitted file`,
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
`,
|
||||||
|
'example.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' }, undefined);
|
||||||
|
|
||||||
|
await showReport();
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
await expect.soft(page.locator('text="my sample test"')).toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=metadata-error')).not.toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=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': `
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
metadata: {
|
||||||
|
'revision.timestamp': 'hi',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
`,
|
||||||
|
'example.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('my sample test', async ({}) => { expect(2).toBe(2); });
|
||||||
|
`,
|
||||||
|
}, { reporter: 'dot,html' }, { PW_TEST_HTML_REPORT_OPEN: 'never' });
|
||||||
|
|
||||||
|
await showReport();
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
await expect.soft(page.locator('text="my sample test"')).toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=metadata-error')).toBeVisible();
|
||||||
|
await expect.soft(page.locator('data-test-id=metadata-chip')).not.toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
3
utils/generate_types/overrides-test.d.ts
vendored
3
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -66,6 +66,8 @@ export interface Config<TestArgs = {}, WorkerArgs = {}> extends TestConfig {
|
||||||
use?: UseOptions<TestArgs, WorkerArgs>;
|
use?: UseOptions<TestArgs, WorkerArgs>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Metadata = { [key: string]: string | number | boolean };
|
||||||
|
|
||||||
// [internal] !!! DO NOT ADD TO THIS !!!
|
// [internal] !!! DO NOT ADD TO THIS !!!
|
||||||
// [internal] It is part of the public API and is computed from the user's config.
|
// [internal] It is part of the public API and is computed from the user's config.
|
||||||
// [internal] If you need new fields internally, add them to FullConfigInternal instead.
|
// [internal] If you need new fields internally, add them to FullConfigInternal instead.
|
||||||
|
|
@ -78,6 +80,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
grep: RegExp | RegExp[];
|
grep: RegExp | RegExp[];
|
||||||
grepInvert: RegExp | RegExp[] | null;
|
grepInvert: RegExp | RegExp[] | null;
|
||||||
maxFailures: number;
|
maxFailures: number;
|
||||||
|
metadata: Metadata;
|
||||||
version: string;
|
version: string;
|
||||||
preserveOutput: 'always' | 'never' | 'failures-only';
|
preserveOutput: 'always' | 'never' | 'failures-only';
|
||||||
projects: FullProject<TestArgs, WorkerArgs>[];
|
projects: FullProject<TestArgs, WorkerArgs>[];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue