From bd86e704650192cd4b839e44fe1ee1d820973716 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 24 Jun 2021 10:02:34 +0200 Subject: [PATCH] feat(test-runner): allow to focus a test in a location (#7208) --- src/test/cli.ts | 10 ++- src/test/runner.ts | 33 +++++++-- src/test/util.ts | 5 ++ tests/playwright-test/test-ignore.spec.ts | 89 +++++++++++++++++++++++ 4 files changed, 128 insertions(+), 9 deletions(-) diff --git a/src/test/cli.ts b/src/test/cli.ts index 1aa604e4d6..d6bc126cf6 100644 --- a/src/test/cli.ts +++ b/src/test/cli.ts @@ -22,6 +22,7 @@ import * as path from 'path'; import type { Config } from './types'; import { Runner } from './runner'; import { stopProfiling, startProfiling } from './profiler'; +import type { FilePatternFilter } from './util'; const defaultTimeout = 30000; const defaultReporter = process.env.CI ? 'dot' : 'list'; @@ -131,7 +132,14 @@ async function runTests(args: string[], opts: { [key: string]: any }) { runner.loadEmptyConfig(process.cwd()); } - const result = await runner.run(!!opts.list, args.map(forceRegExp), opts.project || undefined); + const filePatternFilters: FilePatternFilter[] = args.map(arg => { + const match = /^(.*):(\d+)$/.exec(arg); + return { + re: forceRegExp(match ? match[1] : arg), + line: match ? parseInt(match[2], 10) : null, + }; + }); + const result = await runner.run(!!opts.list, filePatternFilters, opts.project || undefined); await stopProfiling(undefined); if (result === 'sigint') diff --git a/src/test/runner.ts b/src/test/runner.ts index 35c9ef5ad1..591b9ddbd5 100644 --- a/src/test/runner.ts +++ b/src/test/runner.ts @@ -21,8 +21,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { Dispatcher } from './dispatcher'; -import { createMatcher, monotonicTime, raceAgainstDeadline } from './util'; -import { Suite } from './test'; +import { createMatcher, FilePatternFilter, monotonicTime, raceAgainstDeadline } from './util'; +import { Spec, Suite } from './test'; import { Loader } from './loader'; import { Reporter } from './reporter'; import { Multiplexer } from './reporters/multiplexer'; @@ -81,11 +81,11 @@ export class Runner { this._loader.loadEmptyConfig(rootDir); } - async run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise { + async run(list: boolean, filePatternFilters: FilePatternFilter[], projectName?: string): Promise { this._reporter = this._createReporter(); const config = this._loader.fullConfig(); const globalDeadline = config.globalTimeout ? config.globalTimeout + monotonicTime() : undefined; - const { result, timedOut } = await raceAgainstDeadline(this._run(list, testFileReFilters, projectName), globalDeadline); + const { result, timedOut } = await raceAgainstDeadline(this._run(list, filePatternFilters, projectName), globalDeadline); if (timedOut) { if (!this._didBegin) this._reporter.onBegin(config, new Suite('')); @@ -119,8 +119,8 @@ export class Runner { await new Promise(f => process.stderr.on('drain', f)); } - async _run(list: boolean, testFileReFilters: RegExp[], projectName?: string): Promise { - const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters) : () => true; + async _run(list: boolean, testFileReFilters: FilePatternFilter[], projectName?: string): Promise { + const testFileFilter = testFileReFilters.length ? createMatcher(testFileReFilters.map(e => e.re)) : () => true; const config = this._loader.fullConfig(); const projects = this._loader.projects().filter(project => { @@ -163,6 +163,7 @@ export class Runner { if (config.forbidOnly && rootSuite._hasOnly()) return 'forbid-only'; filterOnly(rootSuite); + filterByFocusedLine(rootSuite, testFileReFilters); const fileSuites = new Map(); for (const fileSuite of rootSuite.suites) @@ -243,8 +244,24 @@ export class Runner { } function filterOnly(suite: Suite) { - const onlySuites = suite.suites.filter(child => filterOnly(child) || child._only); - const onlyTests = suite.specs.filter(spec => spec._only); + const suiteFilter = (suite: Suite) => suite._only; + const specFilter = (spec: Spec) => spec._only; + return filterSuite(suite, suiteFilter, specFilter); +} + +function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilter[]) { + const testFileLineMatches = (specFileName: string, specLine: number) => focusedTestFileLines.some(({re, line}) => { + re.lastIndex = 0; + return re.test(specFileName) && (line === specLine || line === null); + }); + const suiteFilter = (suite: Suite) => testFileLineMatches(suite.file, suite.line); + const specFilter = (spec: Spec) => testFileLineMatches(spec.file, spec.line); + return filterSuite(suite, suiteFilter, specFilter); +} + +function filterSuite(suite: Suite, suiteFilter: (suites: Suite) => boolean, specFilter: (spec: Spec) => boolean) { + const onlySuites = suite.suites.filter(child => filterSuite(child, suiteFilter, specFilter) || suiteFilter(child)); + const onlyTests = suite.specs.filter(specFilter); const onlyEntries = new Set([...onlySuites, ...onlyTests]); if (onlyEntries.size) { suite.suites = onlySuites; diff --git a/src/test/util.ts b/src/test/util.ts index 18190e758d..5d35219729 100644 --- a/src/test/util.ts +++ b/src/test/util.ts @@ -91,6 +91,11 @@ export function isRegExp(e: any): e is RegExp { export type Matcher = (value: string) => boolean; +export type FilePatternFilter = { + re: RegExp; + line: number | null; +}; + export function createMatcher(patterns: string | RegExp | (string | RegExp)[]): Matcher { const reList: RegExp[] = []; const filePatterns: string[] = []; diff --git a/tests/playwright-test/test-ignore.spec.ts b/tests/playwright-test/test-ignore.spec.ts index 4b33fbe634..10021c2f54 100644 --- a/tests/playwright-test/test-ignore.spec.ts +++ b/tests/playwright-test/test-ignore.spec.ts @@ -211,6 +211,23 @@ test('should match regex string argument', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); +test('should match regex string with a colon argument', async ({ runInlineTest }) => { + test.skip(process.platform === 'win32', 'Windows does not support colons in the file name'); + const result = await runInlineTest({ + 'fileb.test.ts': ` + const { test } = pwt; + test('pass', ({}) => {}); + `, + 'weird:file.test.ts': ` + const { test } = pwt; + test('pass', ({}) => {}); + ` + }, { args: ['/weird:file\.test\.ts/'] }); + expect(result.passed).toBe(1); + expect(result.report.suites.map(s => s.file).sort()).toEqual(['weird:file.test.ts']); + expect(result.exitCode).toBe(0); +}); + test('should match case insensitive', async ({ runInlineTest }) => { const result = await runInlineTest({ 'capital/A.test.ts': ` @@ -231,6 +248,78 @@ test('should match case insensitive', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); +test('should focus a single test spec', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'foo.test.ts': ` + const { test } = pwt; + test('pass1', ({}) => {}); + test('pass2', ({}) => {}); + test('pass3', ({}) => {}); + `, + 'bar.test.ts': ` + const { test } = pwt; + test('no-pass1', ({}) => {}); + `, + }, { args: ['foo.test.ts:7'] }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.skipped).toBe(0); + expect(result.report.suites[0].specs[0].title).toEqual('pass2'); +}); + +test('should focus a single nested test spec', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'foo.test.ts': ` + const { test } = pwt; + test('pass1', ({}) => {}); + test.describe('suite-1', () => { + test.describe('suite-2', () => { + test('pass2', ({}) => {}); + }); + }); + test('pass3', ({}) => {}); + `, + 'bar.test.ts': ` + const { test } = pwt; + test('pass3', ({}) => {}); + `, + 'noooo.test.ts': ` + const { test } = pwt; + test('no-pass1', ({}) => {}); + `, + }, { args: ['foo.test.ts:9', 'bar.test.ts'] }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(result.skipped).toBe(0); + expect(result.report.suites[0].specs[0].title).toEqual('pass3'); + expect(result.report.suites[1].suites[0].suites[0].specs[0].title).toEqual('pass2'); +}); + +test('should focus a single test suite', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'foo.test.ts': ` + const { test } = pwt; + test('pass1', ({}) => {}); + test.describe('suite-1', () => { + test.describe('suite-2', () => { + test('pass2', ({}) => {}); + test('pass3', ({}) => {}); + }); + }); + test('pass4', ({}) => {}); + `, + 'bar.test.ts': ` + const { test } = pwt; + test('no-pass1', ({}) => {}); + `, + }, { args: ['foo.test.ts:8'] }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); + expect(result.skipped).toBe(0); + expect(result.report.suites[0].suites[0].suites[0].specs[0].title).toEqual('pass2'); + expect(result.report.suites[0].suites[0].suites[0].specs[1].title).toEqual('pass3'); +}); + test('should match by directory', async ({ runInlineTest }) => { const result = await runInlineTest({ 'dir-a/file.test.ts': `