diff --git a/packages/playwright-test/src/common/config.ts b/packages/playwright-test/src/common/config.ts
index a3c443a277..ff61f2ac51 100644
--- a/packages/playwright-test/src/common/config.ts
+++ b/packages/playwright-test/src/common/config.ts
@@ -259,7 +259,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] |
return reporters;
}
-export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob'] as const;
+export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const;
export type BuiltInReporter = typeof builtInReporters[number];
export type ContextReuseMode = 'none' | 'force' | 'when-possible';
diff --git a/packages/playwright-test/src/reporters/markdown.ts b/packages/playwright-test/src/reporters/markdown.ts
new file mode 100644
index 0000000000..8152b45549
--- /dev/null
+++ b/packages/playwright-test/src/reporters/markdown.ts
@@ -0,0 +1,75 @@
+/**
+ * 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 fs from 'fs';
+import path from 'path';
+import type { FullResult, TestCase } from '../../types/testReporter';
+import { BaseReporter, formatTestTitle } from './base';
+
+type MarkdownReporterOptions = {
+ configDir: string,
+ outputFile?: string;
+};
+
+
+class MarkdownReporter extends BaseReporter {
+ private _options: MarkdownReporterOptions;
+
+ constructor(options: MarkdownReporterOptions) {
+ super();
+ this._options = options;
+ }
+
+ printsToStdio() {
+ return false;
+ }
+
+ override async onEnd(result: FullResult) {
+ await super.onEnd(result);
+ const summary = this.generateSummary();
+ const lines: string[] = [];
+ lines.push(`:x: failed: ${summary.unexpected.length}`);
+ this._printTestList(summary.unexpected, lines);
+ if (summary.flaky.length) {
+ lines.push(`:warning: flaky: ${summary.flaky.length}`);
+ this._printTestList(summary.flaky, lines);
+ }
+ if (summary.interrupted.length) {
+ lines.push(`:warning: interrupted: ${summary.interrupted.length}`);
+ this._printTestList(summary.interrupted, lines);
+ }
+ if (summary.skipped) {
+ lines.push(`:ballot_box_with_check: skipped: ${summary.skipped}`);
+ lines.push(``);
+ }
+ lines.push(`:white_check_mark: passed: ${summary.expected}`);
+ lines.push(``);
+
+ const reportFile = path.resolve(this._options.configDir, this._options.outputFile || 'report.md');
+ await fs.promises.mkdir(path.dirname(reportFile), { recursive: true });
+ await fs.promises.writeFile(reportFile, lines.join('\n'));
+ }
+
+ private _printTestList(tests: TestCase[], lines: string[]) {
+ for (const test of tests)
+ lines.push(` - ${formatTestTitle(this.config, test)}`);
+ lines.push(``);
+ }
+}
+
+
+export default MarkdownReporter;
+
diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts
index 9f1a93cf54..de3d3af937 100644
--- a/packages/playwright-test/src/runner/reporters.ts
+++ b/packages/playwright-test/src/runner/reporters.ts
@@ -25,6 +25,7 @@ import JSONReporter from '../reporters/json';
import JUnitReporter from '../reporters/junit';
import LineReporter from '../reporters/line';
import ListReporter from '../reporters/list';
+import MarkdownReporter from '../reporters/markdown';
import type { Suite } from '../common/test';
import type { BuiltInReporter, FullConfigInternal } from '../common/config';
import { loadReporter } from './loadUtils';
@@ -42,6 +43,7 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
null: EmptyReporter,
html: mode === 'ui' ? LineReporter : HtmlReporter,
blob: BlobReporter,
+ markdown: MarkdownReporter,
};
const reporters: Reporter[] = [];
descriptions ??= config.config.reporter;
diff --git a/tests/playwright-test/reporter-markdown.spec.ts b/tests/playwright-test/reporter-markdown.spec.ts
new file mode 100644
index 0000000000..55a9b992c1
--- /dev/null
+++ b/tests/playwright-test/reporter-markdown.spec.ts
@@ -0,0 +1,101 @@
+/**
+ * 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 * as fs from 'fs';
+import { expect, test } from './playwright-test-fixtures';
+
+test('simple report', async ({ runInlineTest }) => {
+ const files = {
+ 'playwright.config.ts': `
+ module.exports = {
+ retries: 1,
+ reporter: 'markdown',
+ };
+ `,
+ 'a.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('math 1', async ({}) => {
+ expect(1 + 1).toBe(2);
+ });
+ test('failing 1', async ({}) => {
+ expect(1).toBe(2);
+ });
+ test('flaky 1', async ({}) => {
+ expect(test.info().retry).toBe(1);
+ });
+ test.skip('skipped 1', async ({}) => {});
+ `,
+ 'b.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('math 2', async ({}) => {
+ expect(1 + 1).toBe(2);
+ });
+ test('failing 2', async ({}) => {
+ expect(1).toBe(2);
+ });
+ test.skip('skipped 2', async ({}) => {});
+ `,
+ 'c.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('math 3', async ({}) => {
+ expect(1 + 1).toBe(2);
+ });
+ test('flaky 2', async ({}) => {
+ expect(test.info().retry).toBe(1);
+ });
+ test.skip('skipped 3', async ({}) => {});
+ `
+ };
+ const { exitCode } = await runInlineTest(files);
+ expect(exitCode).toBe(1);
+ const reportFile = await fs.promises.readFile(test.info().outputPath('report.md'));
+ expect(reportFile.toString()).toBe(`:x: failed: 2
+ - a.test.js:6:11 › failing 1
+ - b.test.js:6:11 › failing 2
+
+:warning: flaky: 2
+ - a.test.js:9:11 › flaky 1
+ - c.test.js:6:11 › flaky 2
+
+:ballot_box_with_check: skipped: 3
+
+:white_check_mark: passed: 3
+`);
+});
+
+test('custom report file', async ({ runInlineTest }) => {
+ const files = {
+ 'playwright.config.ts': `
+ module.exports = {
+ reporter: [['markdown', { outputFile: 'my-report.md' }]],
+ };
+ `,
+ 'a.test.js': `
+ import { test, expect } from '@playwright/test';
+ test('math 1', async ({}) => {
+ expect(1 + 1).toBe(2);
+ });
+ `,
+ };
+
+ const { exitCode } = await runInlineTest(files);
+ expect(exitCode).toBe(0);
+ const reportFile = await fs.promises.readFile(test.info().outputPath('my-report.md'));
+ expect(reportFile.toString()).toBe(`:x: failed: 0
+
+:white_check_mark: passed: 1
+`);
+});
\ No newline at end of file