From a2eb43b3356e6ed48b2a1d1c16cf5f649ac63f18 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 25 Apr 2024 08:15:27 -0700 Subject: [PATCH] feat(runner): allow running last failed tests (#30533) Fixes: https://github.com/microsoft/playwright/issues/30506 --- packages/playwright/src/program.ts | 8 +++++++- packages/playwright/src/runner/runner.ts | 25 ++++++++++++++++++++++++ tests/playwright-test/runner.spec.ts | 21 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 018eb5660d..7f7e1cae40 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -19,7 +19,7 @@ import type { Command } from 'playwright-core/lib/utilsBundle'; import fs from 'fs'; import path from 'path'; -import { Runner } from './runner/runner'; +import { Runner, readLastRunInfo } from './runner/runner'; import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; import { serializeError } from './util'; import { showHTMLReport } from './reporters/html'; @@ -183,6 +183,11 @@ async function runTests(args: string[], opts: { [key: string]: any }) { if (!config) return; + if (opts.lastFailed) { + const lastRunInfo = await readLastRunInfo(config); + config.testIdMatcher = id => lastRunInfo.failedTests.includes(id); + } + config.cliArgs = args; config.cliGrep = opts.grep as string | undefined; config.cliGrepInvert = opts.grepInvert as string | undefined; @@ -338,6 +343,7 @@ const testOptions: [string, string][] = [ ['-gv, --grep-invert ', `Only run tests that do not match this regular expression`], ['--headed', `Run tests in headed browsers (default: headless)`], ['--ignore-snapshots', `Ignore screenshot and snapshot expectations`], + ['--last-failed', `Run last failed tests`], ['--list', `Collect all the tests and report them, but do not run`], ['--max-failures ', `Stop after the first N failures`], ['--no-deps', 'Do not run project dependencies'], diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index 3e4b3c6a78..05d3640ac5 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import fs from 'fs'; import path from 'path'; import { monotonicTime } from 'playwright-core/lib/utils'; import type { FullResult, TestError } from '../../types/testReporter'; @@ -93,6 +94,8 @@ export class Runner { if (modifiedResult && modifiedResult.status) status = modifiedResult.status; + await writeLastRunInfo(testRun, status); + await reporter.onExit(); // Calling process.exit() might truncate large stdout/stderr output. @@ -144,3 +147,25 @@ export class Runner { return { testFiles: affectedTestFiles(resolvedFiles) }; } } + +export type LastRunInfo = { + status: FullResult['status']; + failedTests: string[]; +}; + +async function writeLastRunInfo(testRun: TestRun, status: FullResult['status']) { + await fs.promises.mkdir(testRun.config.globalOutputDir, { recursive: true }); + const lastRunReportFile = path.join(testRun.config.globalOutputDir, 'last-run.json'); + const failedTests = testRun.rootSuite?.allTests().filter(t => !t.ok()).map(t => t.id); + const lastRunReport = JSON.stringify({ status, failedTests }, undefined, 2); + await fs.promises.writeFile(lastRunReportFile, lastRunReport); +} + +export async function readLastRunInfo(config: FullConfigInternal): Promise { + const lastRunReportFile = path.join(config.globalOutputDir, 'last-run.json'); + try { + return JSON.parse(await fs.promises.readFile(lastRunReportFile, 'utf8')) as LastRunInfo; + } catch (e) { + } + return { status: 'passed', failedTests: [] }; +} diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index 113318faac..955c8e1c0c 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -818,3 +818,24 @@ test('wait for workers to finish before reporter.onEnd', async ({ runInlineTest expect(secondIndex).not.toBe(-1); expect(secondIndex).toBeLessThan(endIndex); }); + +test('should run last failed tests', async ({ runInlineTest }) => { + const workspace = { + 'a.spec.js': ` + import { test, expect } from '@playwright/test'; + test('pass', async () => {}); + test('fail', async () => { + expect(1).toBe(2); + }); + ` + }; + const result1 = await runInlineTest(workspace); + expect(result1.exitCode).toBe(1); + expect(result1.passed).toBe(1); + expect(result1.failed).toBe(1); + + const result2 = await runInlineTest(workspace, {}, {}, { additionalArgs: ['--last-failed'] }); + expect(result2.exitCode).toBe(1); + expect(result2.passed).toBe(0); + expect(result2.failed).toBe(1); +});