diff --git a/docs/src/test-api/class-globalinfo.md b/docs/src/test-api/class-globalinfo.md
deleted file mode 100644
index e220e74703..0000000000
--- a/docs/src/test-api/class-globalinfo.md
+++ /dev/null
@@ -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`].
diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md
index 29a01f8642..c78ea12a32 100644
--- a/docs/src/test-api/class-testconfig.md
+++ b/docs/src/test-api/class-testconfig.md
@@ -249,9 +249,9 @@ export default config;
```
## 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
- type: ?<[string]>
diff --git a/docs/src/test-reporter-api/class-suite.md b/docs/src/test-reporter-api/class-suite.md
index e5283fb370..ddf642bd00 100644
--- a/docs/src/test-reporter-api/class-suite.md
+++ b/docs/src/test-reporter-api/class-suite.md
@@ -63,12 +63,3 @@ Suite title.
- returns: <[Array]<[string]>>
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`].
diff --git a/packages/html-reporter/src/chip.tsx b/packages/html-reporter/src/chip.tsx
index 20627c7a50..65f8863988 100644
--- a/packages/html-reporter/src/chip.tsx
+++ b/packages/html-reporter/src/chip.tsx
@@ -26,8 +26,9 @@ export const Chip: React.FC<{
noInsets?: boolean,
setExpanded?: (expanded: boolean) => void,
children?: any,
-}> = ({ header, expanded, setExpanded, children, noInsets }) => {
- return
+ dataTestId?: string,
+}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId }) => {
+ return
setExpanded?.(!expanded)}
@@ -45,13 +46,15 @@ export const AutoChip: React.FC<{
initialExpanded?: boolean,
noInsets?: boolean,
children?: any,
-}> = ({ header, initialExpanded, noInsets, children }) => {
+ dataTestId?: string,
+}> = ({ header, initialExpanded, noInsets, children, dataTestId }) => {
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
return
{children}
;
diff --git a/packages/html-reporter/src/index.tsx b/packages/html-reporter/src/index.tsx
index d2be445e5d..e26a01592c 100644
--- a/packages/html-reporter/src/index.tsx
+++ b/packages/html-reporter/src/index.tsx
@@ -14,7 +14,7 @@
* 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';
// @ts-ignore
import zipImport from '@zip.js/zip.js/dist/zip-no-worker-inflate.min.js';
@@ -26,59 +26,6 @@ import { ReportView } from './reportView';
// @ts-ignore
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 [report, setReport] = React.useState
();
React.useEffect(() => {
@@ -96,14 +43,13 @@ window.onload = () => {
class ZipReport implements LoadedReport {
private _entries = new Map();
- private _json!: HTMLReport & { metadata?: Metadata };
+ private _json!: HTMLReport;
async load() {
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader((window as any).playwrightReportBase64), { useWebWorkers: false }) as zip.ZipReader;
for (const entry of await zipReader.getEntries())
this._entries.set(entry.filename, entry);
this._json = await this.entry('report.json') as HTMLReport;
- this._json.metadata = extractMetadata(this._json.attachments);
}
json(): HTMLReport {
diff --git a/packages/html-reporter/src/loadedReport.ts b/packages/html-reporter/src/loadedReport.ts
index e4d01d8c52..80606bf3b6 100644
--- a/packages/html-reporter/src/loadedReport.ts
+++ b/packages/html-reporter/src/loadedReport.ts
@@ -15,9 +15,8 @@
*/
import type { HTMLReport } from '@playwright-test/reporters/html';
-import type { Metadata } from './index';
export interface LoadedReport {
- json(): HTMLReport & { metadata?: Metadata };
+ json(): HTMLReport;
entry(name: string): Promise;
}
diff --git a/packages/html-reporter/src/metadataView.tsx b/packages/html-reporter/src/metadataView.tsx
index 16782fc00f..8c3d481878 100644
--- a/packages/html-reporter/src/metadataView.tsx
+++ b/packages/html-reporter/src/metadataView.tsx
@@ -18,21 +18,61 @@ import * as React from 'react';
import './colors.css';
import './common.css';
import * as icons from './icons';
-import type { Metadata } from './index';
import { AutoChip } from './chip';
import './reportView.css';
import './theme.css';
-export const MetadataView: React.FC = 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 (
+
+ An error was encountered when trying to render Commit Metainfo. Please file a GitHub issue to report this error.
+
+
{this.state.error?.message} {this.state.error?.stack} {this.state.errorInfo?.componentStack}
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export const MetadataView: React.FC = metadata => ;
+
+const InnerMetadataView: React.FC = metadata => {
+ if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
+ return null;
+
return (
{metadata['revision.id'] &&
{metadata['revision.id'].slice(0, 7)}
}
- {metadata['revision.subject'] && metadata['revision.subject'] || 'no subject>'}
- {!metadata['revision.subject'] && 'Commit metainfo'}
- } initialExpanded={false}>
+ {metadata['revision.subject'] || 'Commit Metainfo'}
+ } initialExpanded={false} dataTestId='metadata-chip'>
{metadata['revision.subject'] &&
= metadata => {
icon='externalLink'
/>
}
- {metadata['generatedAt'] &&
+ {metadata['timestamp'] &&
- 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'])}
}>
}
diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx
index f65e7b2b88..2594160e88 100644
--- a/packages/html-reporter/src/reportView.tsx
+++ b/packages/html-reporter/src/reportView.tsx
@@ -23,6 +23,7 @@ import { HeaderView } from './headerView';
import { Route } from './links';
import type { LoadedReport } from './loadedReport';
import './reportView.css';
+import type { Metainfo } from './metadataView';
import { MetadataView } from './metadataView';
import { TestCaseView } from './testCaseView';
import { TestFilesView } from './testFilesView';
@@ -46,7 +47,7 @@ export const ReportView: React.FC<{
return
{report?.json() && }
- {report?.json().metadata && }
+ {report?.json().metadata && }
diff --git a/packages/playwright-test/src/ci.ts b/packages/playwright-test/src/ci.ts
deleted file mode 100644
index a259f914be..0000000000
--- a/packages/playwright-test/src/ci.ts
+++ /dev/null
@@ -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 => {
- 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 => {
- 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 => {
- // 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 => {
- // 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 => {
- return [{ name: 'generatedAt', body: Buffer.from(JSON.stringify(Date.now())), contentType: kContentTypeJSON }];
-};
diff --git a/packages/playwright-test/src/globalInfo.ts b/packages/playwright-test/src/globalInfo.ts
deleted file mode 100644
index 28d92256e3..0000000000
--- a/packages/playwright-test/src/globalInfo.ts
+++ /dev/null
@@ -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));
- }
-}
diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts
index cb8611c90a..3eecf0a6d0 100644
--- a/packages/playwright-test/src/loader.ts
+++ b/packages/playwright-test/src/loader.ts
@@ -15,7 +15,7 @@
*/
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 { getPackageJsonPath, mergeObjects, errorWithFile } from './util';
import { setCurrentlyLoadingFileSuite } from './globals';
@@ -163,6 +163,7 @@ export class Loader {
this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers);
this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer);
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));
}
@@ -210,7 +211,7 @@ export class Loader {
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);
if (hook && typeof hook === 'object' && ('default' in hook))
hook = hook['default'];
@@ -516,6 +517,7 @@ export const baseFullConfig: FullConfigInternal = {
grep: /.*/,
grepInvert: null,
maxFailures: 0,
+ metadata: {},
preserveOutput: 'always',
projects: [],
reporter: [ [process.env.CI ? 'dot' : 'list'] ],
diff --git a/packages/playwright-test/src/plugins/gitCommitInfoPlugin.ts b/packages/playwright-test/src/plugins/gitCommitInfoPlugin.ts
new file mode 100644
index 0000000000..b0ae3ddaf4
--- /dev/null
+++ b/packages/playwright-test/src/plugins/gitCommitInfoPlugin.ts
@@ -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 => {
+ 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 => {
+ 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,
+ };
+};
diff --git a/packages/playwright-test/src/plugins/index.ts b/packages/playwright-test/src/plugins/index.ts
index a337ddc086..999abdc782 100644
--- a/packages/playwright-test/src/plugins/index.ts
+++ b/packages/playwright-test/src/plugins/index.ts
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { webServer } from './webServerPlugin';
+export { gitCommitInfo } from './gitCommitInfoPlugin';
diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts
index 7607e86a95..07de403e2e 100644
--- a/packages/playwright-test/src/reporters/html.ts
+++ b/packages/playwright-test/src/reporters/html.ts
@@ -28,7 +28,7 @@ import type { JsonAttachment, JsonReport, JsonSuite, JsonTestCase, JsonTestResul
import RawReporter from './raw';
import { stripAnsiEscapes } from './base';
import { getPackageJsonPath } from '../util';
-import type { FullConfigInternal } from '../types';
+import type { FullConfigInternal, Metadata } from '../types';
import type { ZipFile } from 'playwright-core/lib/zipBundle';
import { yazl } from 'playwright-core/lib/zipBundle';
@@ -49,7 +49,7 @@ export type Location = {
};
export type HTMLReport = {
- attachments: TestAttachment[];
+ metadata: Metadata;
files: TestFileSummary[];
stats: Stats;
projectNames: string[];
@@ -159,12 +159,12 @@ class HtmlReporter implements Reporter {
const projectSuites = this.suite.suites;
const reports = projectSuites.map(suite => {
const rawReporter = new RawReporter();
- const report = rawReporter.generateProjectReport(this.config, suite, []);
+ const report = rawReporter.generateProjectReport(this.config, suite);
return report;
});
await removeFolders([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)
return;
@@ -255,7 +255,7 @@ class HtmlBuilder {
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();
for (const projectJson of rawReports) {
@@ -311,7 +311,7 @@ class HtmlBuilder {
this._addDataFile(fileId + '.json', testFile);
}
const htmlReport: HTMLReport = {
- attachments: this._serializeAttachments(testReportAttachments),
+ metadata,
files: [...data.values()].map(e => e.testFileSummary),
projectNames: rawReports.map(r => r.project.name),
stats: [...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts
index 5752d57189..8f9cafe8ca 100644
--- a/packages/playwright-test/src/reporters/raw.ts
+++ b/packages/playwright-test/src/reporters/raw.ts
@@ -23,6 +23,7 @@ import { formatResultFailure } from './base';
import { toPosixPath, serializePatterns } from './json';
import { MultiMap } from 'playwright-core/lib/utils/multimap';
import { codeFrameColumns } from '../babelBundle';
+import type { Metadata } from '../types';
export type JsonLocation = Location;
export type JsonError = string;
@@ -30,7 +31,6 @@ export type JsonStackFrame = { file: string, line: number, column: number };
export type JsonReport = {
config: JsonConfig,
- attachments: JsonAttachment[],
project: JsonProject,
suites: JsonSuite[],
};
@@ -38,7 +38,7 @@ export type JsonReport = {
export type JsonConfig = Omit;
export type JsonProject = {
- metadata: any,
+ metadata: Metadata,
name: string,
outputDir: string,
repeatEach: number,
@@ -112,7 +112,6 @@ class RawReporter {
async onEnd() {
const projectSuites = this.suite.suites;
- const globalAttachments = this.generateAttachments(this.suite.attachments);
for (const suite of projectSuites) {
const project = suite.project();
assert(project, 'Internal Error: Invalid project structure');
@@ -130,7 +129,7 @@ class RawReporter {
}
if (!reportFile)
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));
}
}
@@ -163,13 +162,12 @@ class RawReporter {
return out;
}
- generateProjectReport(config: FullConfig, suite: Suite, attachments: JsonAttachment[]): JsonReport {
+ generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
this.config = config;
const project = suite.project();
assert(project, 'Internal Error: Invalid project structure');
const report: JsonReport = {
config,
- attachments,
project: {
metadata: project.metadata,
name: project.name,
diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts
index 4ab1738912..b4d13efb96 100644
--- a/packages/playwright-test/src/runner.ts
+++ b/packages/playwright-test/src/runner.ts
@@ -42,7 +42,6 @@ import type { Config, FullProjectInternal } from './types';
import type { FullConfigInternal } from './types';
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner';
import { SigIntWatcher } from './sigIntWatcher';
-import { GlobalInfoImpl } from './globalInfo';
const removeFolderAsync = promisify(rimraf);
const readDirAsync = promisify(fs.readdir);
@@ -78,11 +77,9 @@ export type ConfigCLIOverrides = {
export class Runner {
private _loader: Loader;
private _reporter!: Reporter;
- private _globalInfo: GlobalInfoImpl;
constructor(configCLIOverrides?: ConfigCLIOverrides) {
this._loader = new Loader(configCLIOverrides);
- this._globalInfo = new GlobalInfoImpl(this._loader.fullConfig());
}
async loadConfigFromResolvedFile(resolvedConfigFile: string): Promise {
@@ -398,9 +395,6 @@ export class Runner {
const result: FullResult = { status: 'passed' };
- // 13.5 Add copy of attachments.
- rootSuite.attachments = this._globalInfo.attachments();
-
// 14. Run tests.
try {
const sigintWatcher = new SigIntWatcher();
@@ -466,7 +460,7 @@ export class Runner {
// The do global setup.
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);
if (result.status !== 'passed') {
diff --git a/packages/playwright-test/src/test.ts b/packages/playwright-test/src/test.ts
index d341af2cb4..1f433d4f43 100644
--- a/packages/playwright-test/src/test.ts
+++ b/packages/playwright-test/src/test.ts
@@ -39,7 +39,6 @@ export type Modifier = {
export class Suite extends Base implements reporterTypes.Suite {
suites: Suite[] = [];
tests: TestCase[] = [];
- attachments: reporterTypes.Suite['attachments'] = [];
location?: Location;
parent?: Suite;
_use: FixturesWithLocation[] = [];
diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts
index 8c6cf90304..3e57650f74 100644
--- a/packages/playwright-test/types/test.d.ts
+++ b/packages/playwright-test/types/test.d.ts
@@ -628,9 +628,9 @@ interface TestConfig {
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
@@ -921,6 +921,8 @@ export interface Config extends TestConfig {
use?: UseOptions;
}
+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
* `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 {
*
*/
maxFailures: number;
+ /**
+ * Metadata that will be put directly to the test report serialized as JSON.
+ */
+ metadata: Metadata;
version: string;
/**
* Whether to preserve test output in the
@@ -3501,122 +3507,6 @@ interface ScreenshotAssertions {
}): 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;
-}
-
/**
* Information about an error thrown during test execution.
*/
diff --git a/packages/playwright-test/types/testReporter.d.ts b/packages/playwright-test/types/testReporter.d.ts
index 6cd9a127cf..6b0a9139cb 100644
--- a/packages/playwright-test/types/testReporter.d.ts
+++ b/packages/playwright-test/types/testReporter.d.ts
@@ -84,33 +84,7 @@ export interface Suite {
/**
* Returns a list of titles from the root down to this suite.
*/
- titlePath(): Array;
-
- /**
- * 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;
- }>;}
+ titlePath(): Array;}
/**
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
diff --git a/tests/android/playwright.config.ts b/tests/android/playwright.config.ts
index 1aa3d12f33..42f55df5c3 100644
--- a/tests/android/playwright.config.ts
+++ b/tests/android/playwright.config.ts
@@ -20,13 +20,16 @@ loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import * as path from 'path';
import type { ServerWorkerOptions } from '../config/serverFixtures';
+import { gitCommitInfo } from '@playwright/test/lib/plugins';
process.env.PWPAGE_IMPL = 'android';
const outputDir = path.join(__dirname, '..', '..', 'test-results');
const testDir = path.join(__dirname, '..');
const config: Config = {
- globalSetup: path.join(__dirname, '../config/globalSetup'),
+ plugins: [
+ gitCommitInfo(),
+ ],
testDir,
outputDir,
timeout: 120000,
diff --git a/tests/config/experimental.d.ts b/tests/config/experimental.d.ts
index 059362427b..9eb57bd5b5 100644
--- a/tests/config/experimental.d.ts
+++ b/tests/config/experimental.d.ts
@@ -16865,9 +16865,9 @@ interface TestConfig {
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
@@ -17196,6 +17196,8 @@ export interface Config extends TestConfig {
use?: UseOptions;
}
+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
* `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 {
*
*/
maxFailures: number;
+ /**
+ * Metadata that will be put directly to the test report serialized as JSON.
+ */
+ metadata: Metadata;
version: string;
/**
* Whether to preserve test output in the
@@ -19962,122 +19968,6 @@ interface ScreenshotAssertions {
}): 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;
-}
-
/**
* 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.
*/
- titlePath(): Array;
-
- /**
- * 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;
- }>;}
+ titlePath(): Array;}
/**
* `TestCase` corresponds to every [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
diff --git a/tests/config/globalSetup.ts b/tests/config/globalSetup.ts
deleted file mode 100644
index 2ea4ef32fc..0000000000
--- a/tests/config/globalSetup.ts
+++ /dev/null
@@ -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;
diff --git a/tests/electron/playwright.config.ts b/tests/electron/playwright.config.ts
index b380043ff2..bd717461fd 100644
--- a/tests/electron/playwright.config.ts
+++ b/tests/electron/playwright.config.ts
@@ -20,13 +20,16 @@ loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@playwright/test';
import * as path from 'path';
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
+import { gitCommitInfo } from '@playwright/test/lib/plugins';
process.env.PWPAGE_IMPL = 'electron';
const outputDir = path.join(__dirname, '..', '..', 'test-results');
const testDir = path.join(__dirname, '..');
const config: Config = {
- globalSetup: path.join(__dirname, '../config/globalSetup'),
+ plugins: [
+ gitCommitInfo(),
+ ],
testDir,
outputDir,
timeout: 30000,
diff --git a/tests/installation/playwright.config.ts b/tests/installation/playwright.config.ts
index 78217d407a..bebc4d162f 100644
--- a/tests/installation/playwright.config.ts
+++ b/tests/installation/playwright.config.ts
@@ -19,9 +19,13 @@ import type { PlaywrightTestConfig } from '@playwright/test';
import { config as loadEnv } from 'dotenv';
loadEnv({ path: path.join(__dirname, '..', '..', '.env') });
+import { gitCommitInfo } from '@playwright/test/lib/plugins';
+
const config: PlaywrightTestConfig = {
+ plugins: [
+ gitCommitInfo(),
+ ],
testIgnore: '**\/fixture-scripts/**',
- globalSetup: path.join(__dirname, 'globalSetup'),
timeout: 5 * 60 * 1000,
retries: 0,
reporter: process.env.CI ? 'dot' : [['list'], ['html', { open: 'on-failure' }]],
diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts
index e2d394ff53..be4a456fff 100644
--- a/tests/library/har.spec.ts
+++ b/tests/library/har.spec.ts
@@ -694,4 +694,3 @@ it('should not hang on resources served from cache', async ({ contextFactory, se
else
expect(entries.length).toBe(2);
});
-
diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts
index 7dfbd05bb7..9ad08996fa 100644
--- a/tests/library/playwright.config.ts
+++ b/tests/library/playwright.config.ts
@@ -21,6 +21,7 @@ import type { Config, PlaywrightTestOptions, PlaywrightWorkerOptions } from '@pl
import * as path from 'path';
import type { TestModeWorkerOptions } from '../config/testModeFixtures';
import type { CoverageWorkerOptions } from '../config/coverageFixtures';
+import { gitCommitInfo } from '@playwright/test/lib/plugins';
type BrowserName = 'chromium' | 'firefox' | 'webkit';
@@ -44,7 +45,9 @@ const trace = !!process.env.PWTEST_TRACE;
const outputDir = path.join(__dirname, '..', '..', 'test-results');
const testDir = path.join(__dirname, '..');
const config: Config = {
- globalSetup: path.join(__dirname, '../config/globalSetup'),
+ plugins: [
+ gitCommitInfo(),
+ ],
testDir,
outputDir,
expect: {
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index c256fe9941..3de4c993ea 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -704,64 +704,152 @@ test('open tests from required file', async ({ runInlineTest, showReport, page }
]);
});
-test('should include metadata', async ({ runInlineTest, showReport, page }) => {
- const beforeRunPlaywrightTest = async ({ baseDir }: { baseDir: string }) => {
- const execGit = async (args: string[]) => {
- const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
- if (!!code)
- throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`);
- return;
+test.describe('gitCommitInfo plugin', () => {
+ test('should include metadata', async ({ runInlineTest, showReport, page }) => {
+ const beforeRunPlaywrightTest = async ({ baseDir }: { baseDir: string }) => {
+ const execGit = async (args: string[]) => {
+ const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir });
+ if (!!code)
+ 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']);
- 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']);
- };
+ const result = await runInlineTest({
+ 'uncommitted.txt': `uncommitted file`,
+ 'playwright.config.ts': `
+ import path from 'path';
+ import { gitCommitInfo } from '@playwright/test/lib/plugins';
- const result = await runInlineTest({
- 'uncommitted.txt': `uncommitted file`,
- 'globalSetup.ts': `
- import { FullConfig, GlobalInfo } from '@playwright/test';
- import * as ci from '@playwright/test/lib/ci';
+ const config = {
+ plugins: [ gitCommitInfo() ],
+ }
- async function globalSetup(config: FullConfig, globalInfo: GlobalInfo) {
- const pluginResults = await Promise.all([
- ci.generationTimestamp(),
- ci.gitStatusFromCLI(config.rootDir),
- ci.githubEnv(),
- ]);
+ 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 Promise.all(pluginResults.flat().map(attachment => globalInfo.attach(attachment.name, attachment)));
- }
+ await showReport();
- export default globalSetup;
- `,
- 'playwright.config.ts': `
- import path from 'path';
- const config = {
- globalSetup: path.join(__dirname, './globalSetup'),
- }
+ expect(result.exitCode).toBe(0);
+ await page.click('text=awesome commit message');
+ 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=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);
- await page.click('text=awesome commit message');
- 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=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/);
+ const config = {
+ plugins: [ gitCommitInfo({
+ info: {
+ 'revision.id': '1234567890',
+ 'revision.subject': 'a better subject',
+ 'revision.timestamp': new Date(),
+ 'revision.author': 'William',
+ 'revision.email': 'shakespeare@example.local',
+ },
+ }) ],
+ }
+
+ 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();
+ });
});
diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts
index 8ef3ab7974..05ca59f5f9 100644
--- a/utils/generate_types/overrides-test.d.ts
+++ b/utils/generate_types/overrides-test.d.ts
@@ -66,6 +66,8 @@ export interface Config extends TestConfig {
use?: UseOptions;
}
+export type Metadata = { [key: string]: string | number | boolean };
+
// [internal] !!! DO NOT ADD TO THIS !!!
// [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.
@@ -78,6 +80,7 @@ export interface FullConfig {
grep: RegExp | RegExp[];
grepInvert: RegExp | RegExp[] | null;
maxFailures: number;
+ metadata: Metadata;
version: string;
preserveOutput: 'always' | 'never' | 'failures-only';
projects: FullProject[];