chore: move captureGitInfo back into config (#34961)

This commit is contained in:
Pavel Feldman 2025-02-28 07:33:03 -08:00 committed by GitHub
parent 53f38f19a4
commit e033e5aa11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 120 additions and 76 deletions

View File

@ -36,6 +36,26 @@ export default defineConfig({
}); });
``` ```
## property: TestConfig.captureGitInfo
* since: v1.51
- type: ?<[Object]>
- `commit` ?<boolean> Whether to capture commit information such as hash, author, timestamp.
- `diff` ?<boolean> 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 ## property: TestConfig.expect
* since: v1.10 * since: v1.10
- type: ?<[Object]> - 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. 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** **Usage**
```js title="playwright.config.ts" ```js title="playwright.config.ts"

View File

@ -48,6 +48,7 @@ export class FullConfigInternal {
readonly plugins: TestRunnerPluginRegistration[]; readonly plugins: TestRunnerPluginRegistration[];
readonly projects: FullProjectInternal[] = []; readonly projects: FullProjectInternal[] = [];
readonly singleTSConfigPath?: string; readonly singleTSConfigPath?: string;
readonly captureGitInfo: Config['captureGitInfo'];
cliArgs: string[] = []; cliArgs: string[] = [];
cliGrep: string | undefined; cliGrep: string | undefined;
cliGrepInvert: string | undefined; cliGrepInvert: string | undefined;
@ -77,6 +78,7 @@ export class FullConfigInternal {
const privateConfiguration = (userConfig as any)['@playwright/test']; const privateConfiguration = (userConfig as any)['@playwright/test'];
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig); 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.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); this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);

View File

@ -42,12 +42,6 @@ export type CIInfo = {
branch?: string; branch?: string;
}; };
export type UserMetadataWithCommitInfo = {
ci?: CIInfo;
gitCommit?: GitCommitInfo | 'generate';
gitDiff?: string | 'generate';
};
export type MetadataWithCommitInfo = { export type MetadataWithCommitInfo = {
ci?: CIInfo; ci?: CIInfo;
gitCommit?: GitCommitInfo; gitCommit?: GitCommitInfo;

View File

@ -21,30 +21,26 @@ import { spawnAsync } from 'playwright-core/lib/utils';
import type { TestRunnerPlugin } from './'; import type { TestRunnerPlugin } from './';
import type { FullConfig } from '../../types/testReporter'; import type { FullConfig } from '../../types/testReporter';
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import type { GitCommitInfo, CIInfo, UserMetadataWithCommitInfo } from '../isomorphic/types'; import type { GitCommitInfo, CIInfo, MetadataWithCommitInfo } from '../isomorphic/types';
const GIT_OPERATIONS_TIMEOUT_MS = 3000; const GIT_OPERATIONS_TIMEOUT_MS = 3000;
export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => { export const addGitCommitInfoPlugin = (fullConfig: FullConfigInternal) => {
fullConfig.plugins.push({ factory: gitCommitInfoPlugin }); fullConfig.plugins.push({ factory: gitCommitInfoPlugin.bind(null, fullConfig) });
}; };
type GitCommitInfoPluginOptions = { const gitCommitInfoPlugin = (fullConfig: FullConfigInternal): TestRunnerPlugin => {
directory?: string;
};
export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestRunnerPlugin => {
return { return {
name: 'playwright:git-commit-info', name: 'playwright:git-commit-info',
setup: async (config: FullConfig, configDir: string) => { setup: async (config: FullConfig, configDir: string) => {
const metadata = config.metadata as UserMetadataWithCommitInfo; const metadata = config.metadata as MetadataWithCommitInfo;
const ci = await ciInfo(); const ci = await ciInfo();
if (!metadata.ci && ci) if (!metadata.ci && ci)
metadata.ci = ci; metadata.ci = ci;
if ((ci && !metadata.gitCommit) || metadata.gitCommit === 'generate') { if (fullConfig.captureGitInfo?.commit || (fullConfig.captureGitInfo?.commit === undefined && ci)) {
const git = await gitCommitInfo(options?.directory || configDir).catch(e => { const git = await gitCommitInfo(configDir).catch(e => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Failed to get git commit info', e); console.error('Failed to get git commit info', e);
}); });
@ -52,8 +48,8 @@ export const gitCommitInfoPlugin = (options?: GitCommitInfoPluginOptions): TestR
metadata.gitCommit = git; metadata.gitCommit = git;
} }
if ((ci && !metadata.gitDiff) || metadata.gitDiff === 'generate') { if (fullConfig.captureGitInfo?.diff || (fullConfig.captureGitInfo?.diff === undefined && ci)) {
const diffResult = await gitDiff(options?.directory || configDir, ci).catch(e => { const diffResult = await gitDiff(configDir, ci).catch(e => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Failed to get git diff', e); console.error('Failed to get git diff', e);
}); });

View File

@ -959,6 +959,37 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
external?: Array<string>; external?: Array<string>;
}; };
/**
* - 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). * Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts).
* *
@ -1284,11 +1315,6 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
/** /**
* Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as * Metadata contains key-value pairs to be included in the report. For example, HTML report will display it as
* key-value pairs, and JSON report will include metadata serialized as json. * key-value pairs, and JSON report will include metadata serialized as json.
* - Providing `gitCommit: 'generate'` property will populate it with the git commit details.
* - Providing `gitDiff: 'generate'` property will populate it with the git diff details.
*
* On selected CI providers, both will be generated automatically. Specifying values will prevent the automatic
* generation.
* *
* **Usage** * **Usage**
* *

View File

@ -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 <shakespeare@example\\.local>/
- 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 = { const files = {
'uncommitted.txt': `uncommitted file`, 'uncommitted.txt': `uncommitted file`,
'playwright.config.ts': ` 'playwright.config.ts': `
@ -1201,21 +1237,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
`, `,
}; };
const baseDir = await writeFiles(files); const baseDir = await writeFiles(files);
await 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']);
const result = await runInlineTest(files, { reporter: 'dot,html' }, { const result = await runInlineTest(files, { reporter: 'dot,html' }, {
PLAYWRIGHT_HTML_OPEN: 'never', 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 = { const files = {
'uncommitted.txt': `uncommitted file`, 'uncommitted.txt': `uncommitted file`,
'playwright.config.ts': ` 'playwright.config.ts': `
@ -1255,21 +1277,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
`, `,
}; };
const baseDir = await writeFiles(files); const baseDir = await writeFiles(files);
await 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']);
const eventPath = path.join(baseDir, 'event.json'); const eventPath = path.join(baseDir, 'event.json');
await fs.promises.writeFile(eventPath, JSON.stringify({ 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({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `
export default {}; export default {};
@ -2780,21 +2788,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
`, `,
}; };
const baseDir = await writeFiles(files); const baseDir = await writeFiles(files);
await 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']);
const result = await runInlineTest(files, { reporter: 'dot,html' }, { const result = await runInlineTest(files, { reporter: 'dot,html' }, {
PLAYWRIGHT_HTML_OPEN: 'never', PLAYWRIGHT_HTML_OPEN: 'never',
@ -2831,3 +2825,20 @@ function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
stream.on('end', () => resolve(Buffer.concat(chunks))); 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']);
}