diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index 1f6c774199..57841299ad 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -import fs from 'fs'; import path from 'path'; -import readline from 'readline'; import type { Reporter, TestError } from '../../types/testReporter'; import { InProcessLoaderHost, OutOfProcessLoaderHost } from './loaderHost'; import { Suite } from '../common/test'; @@ -29,6 +27,8 @@ import { requireOrImport } from '../common/transform'; import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { createTestGroups, filterForShard, type TestGroup } from './testGroups'; import { dependenciesForTestFile } from '../common/compilationCache'; +import { sourceMapSupport } from '../utilsBundle'; +import type { RawSourceMap } from 'source-map'; export async function collectProjectsAndTestFiles(config: FullConfigInternal, projectsToIgnore: Set, additionalFileMatcher: Matcher | undefined) { const fsCache = new Map(); @@ -47,15 +47,16 @@ export async function collectProjectsAndTestFiles(config: FullConfigInternal, pr // Filter files based on the file filters, eliminate the empty projects. const filesToRunByProject = new Map(); for (const [project, files] of allFilesForProject) { - const matchedFiles = await Promise.all(files.map(async file => { - if (additionalFileMatcher && !additionalFileMatcher(file)) - return; - if (cliFileMatcher) { - if (!cliFileMatcher(file) && !await isPotentiallyJavaScriptFileWithSourceMap(file, sourceMapCache)) - return; - } - return file; - })); + const matchedFiles = files.filter(file => { + const hasMatchingSources = sourceMapSources(file, sourceMapCache).some(source => { + if (additionalFileMatcher && !additionalFileMatcher(source)) + return false; + if (cliFileMatcher && !cliFileMatcher(source)) + return false; + return true; + }); + return hasMatchingSources; + }); const filteredFiles = matchedFiles.filter(Boolean) as string[]; if (filteredFiles.length) filesToRunByProject.set(project, filteredFiles); @@ -263,29 +264,20 @@ export function loadReporter(config: FullConfigInternal, file: string): Promise< return requireOrImportDefaultFunction(path.resolve(config.rootDir, file), true); } -async function isPotentiallyJavaScriptFileWithSourceMap(file: string, cache: Map): Promise { +function sourceMapSources(file: string, cache: Map): string[] { + let sources = [file]; if (!file.endsWith('.js')) - return false; + return sources; if (cache.has(file)) return cache.get(file)!; try { - const stream = fs.createReadStream(file); - const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); - let lastLine: string | undefined; - rl.on('line', line => { - lastLine = line; - }); - await new Promise((fulfill, reject) => { - rl.on('close', fulfill); - rl.on('error', reject); - stream.on('error', reject); - }); - const hasSourceMap = !!lastLine && lastLine.startsWith('//# sourceMappingURL='); - cache.set(file, hasSourceMap); - return hasSourceMap; - } catch (e) { - cache.set(file, true); - return true; + const sourceMap = sourceMapSupport.retrieveSourceMap(file); + const sourceMapData: RawSourceMap | undefined = typeof sourceMap?.map === 'string' ? JSON.parse(sourceMap.map) : sourceMap?.map; + if (sourceMapData?.sources) + sources = sourceMapData.sources.map(source => path.resolve(path.dirname(file), source)); + } finally { + cache.set(file, sources); + return sources; } } diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index f52fefd99b..ce93a2e42b 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -503,21 +503,26 @@ test('should not load tests not matching filter', async ({ runInlineTest }) => { }); test('should filter by sourcemapped file names', async ({ runInlineTest }) => { - const fileWithSourceMap = `` + -`import {test} from '@playwright/test'; -test.describe('Some describe', ()=>{ - test('Some test', async ()=>{ - console.log('test') - }) -}) -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImdoZXJraW4uZmVhdHVyZSJdLCJuYW1lcyI6WyJOb25lIl0sIm1hcHBpbmdzIjoiQUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUEiLCJmaWxlIjoiZ2hlcmtpbi5mZWF0dXJlIiwic291cmNlc0NvbnRlbnQiOlsiVGVzdCJdfQ==`; - const result = await runInlineTest({ - 'playwright.config.js': `export default { projects: [{}, {}] }`, - 'a.spec.js': fileWithSourceMap, + 'gherkin.spec.js': ` + +import { test } from '@playwright/test'; +test('should run', () => {}); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImdoZXJraW4uZmVhdHVyZSJdLCJuYW1lcyI6WyJOb25lIl0sIm1hcHBpbmdzIjoiQUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUEiLCJmaWxlIjoiZ2hlcmtpbi5mZWF0dXJlIiwic291cmNlc0NvbnRlbnQiOlsiVGVzdCJdfQ==`, + + 'another.spec.js': ` + +throw new Error('should not load another.spec.js'); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFub3RoZXIuZmVhdHVyZSJdLCJuYW1lcyI6WyJOb25lIl0sIm1hcHBpbmdzIjoiQUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUE7QUFBQUEiLCJmaWxlIjoiZ2hlcmtpbi5mZWF0dXJlIiwic291cmNlc0NvbnRlbnQiOlsiVGVzdCJdfQ==`, + + 'nomap.spec.js': ` + +throw new Error('should not load nomap.spec.js');`, }, {}, {}, { additionalArgs: ['gherkin.feature'] }); expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); - expect(result.output).not.toContain('a.spec.js'); + expect(result.passed).toBe(1); + expect(result.output).not.toContain('spec.js'); + expect(result.output).not.toContain('another.feature.js'); + expect(result.output).not.toContain('should not load'); expect(result.output).toContain('gherkin.feature:1'); });