fix(test runner): avoid dependency tracking colliding between esm and cjs

When collecting dependencies both from CJS loader and from ESM loader,
the latter would overwrite the dependencies set instead of appending.

Also make sure cts/cjs/mts/mjs are all supported equally.
This commit is contained in:
Dmitry Gozman 2024-03-18 16:05:09 -07:00
parent 0db1d40abc
commit 1dcc04bb6a
5 changed files with 95 additions and 14 deletions

View file

@ -15,7 +15,7 @@
*/ */
import util from 'util'; import util from 'util';
import { serializeCompilationCache } from '../transform/compilationCache'; import { type SerializedCompilationCache, serializeCompilationCache } from '../transform/compilationCache';
import type { ConfigLocation, FullConfigInternal } from './config'; import type { ConfigLocation, FullConfigInternal } from './config';
import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test'; import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test';
@ -43,7 +43,7 @@ export type ConfigCLIOverrides = {
export type SerializedConfig = { export type SerializedConfig = {
location: ConfigLocation; location: ConfigLocation;
configCLIOverrides: ConfigCLIOverrides; configCLIOverrides: ConfigCLIOverrides;
compilationCache: any; compilationCache?: SerializedCompilationCache;
}; };
export type TtyParams = { export type TtyParams = {

View file

@ -27,7 +27,7 @@ export type MemoryCache = {
moduleUrl?: string; moduleUrl?: string;
}; };
type SerializedCompilationCache = { export type SerializedCompilationCache = {
sourceMaps: [string, string][], sourceMaps: [string, string][],
memoryCache: [string, MemoryCache][], memoryCache: [string, MemoryCache][],
fileDependencies: [string, string[]][], fileDependencies: [string, string[]][],
@ -158,15 +158,19 @@ export function serializeCompilationCache(): SerializedCompilationCache {
}; };
} }
export function addToCompilationCache(payload: any) { export function addToCompilationCache(payload: SerializedCompilationCache) {
for (const entry of payload.sourceMaps) for (const entry of payload.sourceMaps)
sourceMaps.set(entry[0], entry[1]); sourceMaps.set(entry[0], entry[1]);
for (const entry of payload.memoryCache) for (const entry of payload.memoryCache)
memoryCache.set(entry[0], entry[1]); memoryCache.set(entry[0], entry[1]);
for (const entry of payload.fileDependencies) for (const entry of payload.fileDependencies) {
fileDependencies.set(entry[0], new Set(entry[1])); const existing = fileDependencies.get(entry[0]) || [];
for (const entry of payload.externalDependencies) fileDependencies.set(entry[0], new Set([...entry[1], ...existing]));
externalDependencies.set(entry[0], new Set(entry[1])); }
for (const entry of payload.externalDependencies) {
const existing = externalDependencies.get(entry[0]) || [];
externalDependencies.set(entry[0], new Set([...entry[1], ...existing]));
}
} }
function calculateCachePath(filePath: string, hash: string): string { function calculateCachePath(filePath: string, hash: string): string {
@ -249,9 +253,9 @@ const kPlaywrightCoveragePrefix = path.resolve(__dirname, '../../../../tests/con
export function belongsToNodeModules(file: string) { export function belongsToNodeModules(file: string) {
if (file.includes(`${path.sep}node_modules${path.sep}`)) if (file.includes(`${path.sep}node_modules${path.sep}`))
return true; return true;
if (file.startsWith(kPlaywrightInternalPrefix) && file.endsWith('.js')) if (file.startsWith(kPlaywrightInternalPrefix) && (file.endsWith('.js') || file.endsWith('.mjs')))
return true; return true;
if (file.startsWith(kPlaywrightCoveragePrefix) && file.endsWith('.js')) if (file.startsWith(kPlaywrightCoveragePrefix) && (file.endsWith('.js') || file.endsWith('.mjs')))
return true; return true;
return false; return false;
} }

View file

@ -240,7 +240,7 @@ function installTransform(): () => void {
if (!shouldTransform(filename)) if (!shouldTransform(filename))
return code; return code;
return transformHook(code, filename).code; return transformHook(code, filename).code;
}, { exts: ['.ts', '.tsx', '.js', '.jsx', '.mjs'] }); }, { exts: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts', '.cjs', '.cts'] });
return () => { return () => {
reverted = true; reverted = true;

View file

@ -171,7 +171,7 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager = new TimeoutManager(this.project.timeout); this._timeoutManager = new TimeoutManager(this.project.timeout);
this.outputDir = (() => { this.outputDir = (() => {
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, '')); const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|jsx|tsx|mjs|mts|cjs|cts)$/, ''));
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-'); const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' '); const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');

View file

@ -91,8 +91,85 @@ test('should print dependencies in ESM mode', async ({ runInlineTest }) => {
const output = result.output; const output = result.output;
const deps = JSON.parse(output.match(/###(.*)###/)![1]); const deps = JSON.parse(output.match(/###(.*)###/)![1]);
expect(deps).toEqual({ expect(deps).toEqual({
'a.test.ts': ['helperA.ts', 'index.mjs'], 'a.test.ts': ['helperA.ts'],
'b.test.ts': ['helperA.ts', 'helperB.ts', 'index.mjs'], 'b.test.ts': ['helperA.ts', 'helperB.ts'],
});
});
test('should print dependencies in mixed CJS/ESM mode 1', async ({ runInlineTest }) => {
const result = await runInlineTest({
'package.json': `{ "type": "module" }`,
'playwright.config.ts': `
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalTeardown: './globalTeardown.ts',
});
`,
'helperA.cjs': `exports.foo = () => {}`,
'helperB.cjs': `require('./helperA');`,
'a.test.ts': `
import './helperA';
import { test, expect } from '@playwright/test';
test('passes', () => {});
`,
'b.test.cjs': `
require('./helperB');
const { test, expect } = require('@playwright/test');
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependencies } from 'playwright/lib/internalsForTest';
export default () => {
console.log('###' + JSON.stringify(fileDependencies()) + '###');
};
`
}, {});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
const output = result.output;
const deps = JSON.parse(output.match(/###(.*)###/)![1]);
expect(deps).toEqual({
'a.test.ts': ['helperA.cjs'],
'b.test.cjs': ['helperA.cjs', 'helperB.cjs'],
});
});
test('should print dependencies in mixed CJS/ESM mode 2', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.mts': `
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalTeardown: './globalTeardown.ts',
});
`,
'helperA.cjs': `exports.foo = () => {}`,
'helperB.cts': `import './helperA';`,
'a.test.mts': `
import './helperA';
import { test, expect } from '@playwright/test';
test('passes', () => {});
`,
'b.test.ts': `
import './helperB';
const { test, expect } = require('@playwright/test');
test('passes', () => {});
`,
'globalTeardown.ts': `
import { fileDependencies } from 'playwright/lib/internalsForTest';
export default () => {
console.log('###' + JSON.stringify(fileDependencies()) + '###');
};
`
}, {});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
const output = result.output;
const deps = JSON.parse(output.match(/###(.*)###/)![1]);
expect(deps).toEqual({
'a.test.mts': ['helperA.cjs'],
'b.test.ts': ['helperA.cjs', 'helperB.cts'],
}); });
}); });