diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index f837ddbfe8..d6ec36edd7 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -36,6 +36,26 @@ export default defineConfig({ }); ``` +## property: TestConfig.captureGitInfo +* since: v1.51 +- type: ?<[Object]> + - `commit` ? Whether to capture commit information such as hash, author, timestamp. + - `diff` ? Whether to capture commit diff. + +* These settings control whether git information is captured and stored in the config [`property: TestConfig.metadata`]. +* The structure of the git commit metadata is subject to change. +* Default values for these settings depend on the environment. When tests run as a part of CI where it is safe to obtain git information, the default value is true, false otherwise. + +**Usage** + +```js title="playwright.config.ts" +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + captureGitInfo: { commit: true, diff: true } +}); +``` + ## property: TestConfig.expect * since: v1.10 - type: ?<[Object]> @@ -239,11 +259,6 @@ export default defineConfig({ Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as key-value pairs, and JSON report will include metadata serialized as json. -* Providing `gitCommit: 'generate'` property will populate it with the git commit details. -* Providing `gitDiff: 'generate'` property will populate it with the git diff details. - -On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic generation. - **Usage** ```js title="playwright.config.ts" diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index 8d213226ff..0c90b7d290 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -48,6 +48,7 @@ export class FullConfigInternal { readonly plugins: TestRunnerPluginRegistration[]; readonly projects: FullProjectInternal[] = []; readonly singleTSConfigPath?: string; + readonly captureGitInfo: Config['captureGitInfo']; cliArgs: string[] = []; cliGrep: string | undefined; cliGrepInvert: string | undefined; @@ -77,6 +78,7 @@ export class FullConfigInternal { const privateConfiguration = (userConfig as any)['@playwright/test']; this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig); + this.captureGitInfo = userConfig.captureGitInfo; this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); diff --git a/packages/playwright/src/isomorphic/types.d.ts b/packages/playwright/src/isomorphic/types.d.ts index cbdb01cc52..6c211f3594 100644 --- a/packages/playwright/src/isomorphic/types.d.ts +++ b/packages/playwright/src/isomorphic/types.d.ts @@ -42,12 +42,6 @@ export type CIInfo = { branch?: string; }; -export type UserMetadataWithCommitInfo = { - ci?: CIInfo; - gitCommit?: GitCommitInfo | 'generate'; - gitDiff?: string | 'generate'; -}; - export type MetadataWithCommitInfo = { ci?: CIInfo; gitCommit?: GitCommitInfo; diff --git a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts index 9e4370934c..da3f42c915 100644 --- a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts +++ b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts @@ -21,30 +21,26 @@ import { spawnAsync } from 'playwright-core/lib/utils'; import type { TestRunnerPlugin } from './'; import type { FullConfig } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; -import type { GitCommitInfo, CIInfo, UserMetadataWithCommitInfo } from '../isomorphic/types'; +import type { GitCommitInfo, CIInfo, MetadataWithCommitInfo } from '../isomorphic/types'; const GIT_OPERATIONS_TIMEOUT_MS = 3000; export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => { - fullConfig.plugins.push({ factory: gitCommitInfoPlugin }); + fullConfig.plugins.push({ factory: gitCommitInfoPlugin.bind(null, fullConfig) }); }; -type GitCommitInfoPluginOptions = { - directory?: string; -}; - -export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => { +const gitCommitInfoPlugin = (fullConfig: FullConfigInternal): TestRunnerPlugin => { return { name: 'playwright:git-commit-info', setup: async (config: FullConfig, configDir: string) => { - const metadata = config.metadata as UserMetadataWithCommitInfo; + const metadata = config.metadata as MetadataWithCommitInfo; const ci = await ciInfo(); if (!metadata.ci && ci) metadata.ci = ci; - if ((ci && !metadata.gitCommit) || metadata.gitCommit === 'generate') { - const git = await gitCommitInfo(options?.directory || configDir).catch(e => { + if (fullConfig.captureGitInfo?.commit || (fullConfig.captureGitInfo?.commit === undefined && ci)) { + const git = await gitCommitInfo(configDir).catch(e => { // eslint-disable-next-line no-console console.error('Failed to get git commit info', e); }); @@ -52,8 +48,8 @@ export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestR metadata.gitCommit = git; } - if ((ci && !metadata.gitDiff) || metadata.gitDiff === 'generate') { - const diffResult = await gitDiff(options?.directory || configDir, ci).catch(e => { + if (fullConfig.captureGitInfo?.diff || (fullConfig.captureGitInfo?.diff === undefined && ci)) { + const diffResult = await gitDiff(configDir, ci).catch(e => { // eslint-disable-next-line no-console console.error('Failed to get git diff', e); }); diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 2084f11e9f..713c3c9871 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -959,6 +959,37 @@ interface TestConfig { external?: Array; }; + /** + * - These settings control whether git information is captured and stored in the config + * [testConfig.metadata](https://playwright.dev/docs/api/class-testconfig#test-config-metadata). + * - The structure of the git commit metadata is not documented and is a subject to change. + * - Default values for these settings depend on the environment. When tests run as a part of CI where it is safe to + * obtain git information, the default value is true, false otherise. + * + * **Usage** + * + * ```js + * // playwright.config.ts + * import { defineConfig } from '@playwright/test'; + * + * export default defineConfig({ + * captureGitInfo: { commit: true, diff: true } + * }); + * ``` + * + */ + captureGitInfo?: { + /** + * Whether to capture commit information such as hash, author, timestamp. + */ + commit?: boolean; + + /** + * Whether to capture commit diff. + */ + diff?: boolean; + }; + /** * Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts). * @@ -1284,11 +1315,6 @@ interface TestConfig { /** * Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as * key-value pairs, and JSON report will include metadata serialized as json. - * - Providing `gitCommit: 'generate'` property will populate it with the git commit details. - * - Providing `gitDiff: 'generate'` property will populate it with the git diff details. - * - * On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic - * generation. * * **Usage** * diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index cca31f50f1..88faf5695e 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1187,7 +1187,43 @@ for (const useIntermediateMergeReport of [true, false] as const) { ]); }); - test('should include metadata with gitCommit', async ({ runInlineTest, writeFiles, showReport, page }) => { + test('should include commit metadata w/ captureGitInfo', async ({ runInlineTest, writeFiles, showReport, page }) => { + const files = { + 'uncommitted.txt': `uncommitted file`, + 'playwright.config.ts': ` + export default { + captureGitInfo: { commit: true }, + metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] } + }; + `, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(2); }); + `, + }; + const baseDir = await writeFiles(files); + await initGitRepo(baseDir); + + const result = await runInlineTest(files, { reporter: 'dot,html' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + }); + + await showReport(); + + expect(result.exitCode).toBe(0); + await page.getByRole('button', { name: 'Metadata' }).click(); + await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(` + - list: + - listitem: "chore(html): make this test look nice" + - listitem: /William / + - list: + - listitem: "foo : value1" + - listitem: "bar : {\\"prop\\":\\"value2\\"}" + - listitem: "baz : [\\"value3\\",123]" + `); + }); + + test('should include commit metadata w/ CI', async ({ runInlineTest, writeFiles, showReport, page }) => { const files = { 'uncommitted.txt': `uncommitted file`, 'playwright.config.ts': ` @@ -1201,21 +1237,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `, }; const baseDir = await writeFiles(files); - - 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', 'playwright.config.ts']); - await execGit(['commit', '-m', 'init']); - await execGit(['add', '*.ts']); - await execGit(['commit', '-m', 'chore(html): make this test look nice']); + await initGitRepo(baseDir); const result = await runInlineTest(files, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', @@ -1241,7 +1263,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `); }); - test('should include metadata on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => { + test('should include PR metadata on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => { const files = { 'uncommitted.txt': `uncommitted file`, 'playwright.config.ts': ` @@ -1255,21 +1277,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `, }; const baseDir = await writeFiles(files); - - 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', 'playwright.config.ts']); - await execGit(['commit', '-m', 'init']); - await execGit(['add', '*.ts']); - await execGit(['commit', '-m', 'chore(html): make this test look nice']); + await initGitRepo(baseDir); const eventPath = path.join(baseDir, 'event.json'); await fs.promises.writeFile(eventPath, JSON.stringify({ @@ -1306,7 +1314,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `); }); - test('should not include git metadata w/o gitCommit', async ({ runInlineTest, showReport, page }) => { + test('should not include git metadata w/o CI', async ({ runInlineTest, showReport, page }) => { const result = await runInlineTest({ 'playwright.config.ts': ` export default {}; @@ -2780,21 +2788,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { `, }; const baseDir = await writeFiles(files); - - 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', 'playwright.config.ts']); - await execGit(['commit', '-m', 'init']); - await execGit(['add', '*.ts']); - await execGit(['commit', '-m', 'chore(html): make this test look nice']); + await initGitRepo(baseDir); const result = await runInlineTest(files, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', @@ -2831,3 +2825,20 @@ function readAllFromStream(stream: NodeJS.ReadableStream): Promise { stream.on('end', () => resolve(Buffer.concat(chunks))); }); } + +async function initGitRepo(baseDir) { + 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', 'playwright.config.ts']); + await execGit(['commit', '-m', 'init']); + await execGit(['add', '*.ts']); + await execGit(['commit', '-m', 'chore(html): make this test look nice']); +}