feat(test runner): rework how tsconfig is applied
Previously, test runner would load tsconfig for each imported file based on its location, going up the directory tree. Now, test runner mostly uses a single tsconfig for all imported files and respects `files`, `include` and `exclude` properties to determine whether to apply tsconfig to a particular file or not. For backwards compatibility, root tsconfig is used to load `playwright.config.ts`, but when `testDir` has its own tsconfig, it used for loading all tests instead of the root tsconfig.
This commit is contained in:
parent
8f4c2f714d
commit
cb676e29cc
|
|
@ -5,9 +5,9 @@ title: "TypeScript"
|
|||
|
||||
## Introduction
|
||||
|
||||
Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run. Note that Playwright does not check the types and will run tests even if there are non-critical TypeScript compilation errors.
|
||||
Playwright supports TypeScript out of the box. You just write tests in TypeScript, and Playwright will read them, transform to JavaScript and run.
|
||||
|
||||
We recommend you run TypeScript compiler alongside Playwright. For example on GitHub actions:
|
||||
Note that Playwright does not check the types and will run tests even if there are non-critical TypeScript compilation errors. We recommend you run TypeScript compiler alongside Playwright. For example on GitHub actions:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
|
|
@ -28,31 +28,65 @@ npx tsc -p tsconfig.json --noEmit -w
|
|||
|
||||
## tsconfig.json
|
||||
|
||||
Playwright will pick up `tsconfig.json` for each source file it loads. Note that Playwright **only supports** the following tsconfig options: `paths` and `baseUrl`.
|
||||
Playwright will pick up `tsconfig.json` and consult it for each source file it loads. Note that Playwright **only supports** the following tsconfig options: `allowJs`, `baseUrl`, `exclude`, `files`, `include`, `paths`, `references`.
|
||||
|
||||
We recommend setting up a separate `tsconfig.json` in the tests directory so that you can change some preferences specifically for the tests. Here is an example directory structure.
|
||||
We recommend to use the [`references` option](https://www.typescriptlang.org/tsconfig#references), so that you can configure TypeScript differently for source and test files.
|
||||
|
||||
Below is an example directory structure and `tsconfig` file templates.
|
||||
|
||||
```txt
|
||||
src/
|
||||
source.ts
|
||||
|
||||
tests/
|
||||
tsconfig.json # test-specific tsconfig
|
||||
example.spec.ts
|
||||
|
||||
tsconfig.json # generic tsconfig for all typescript sources
|
||||
|
||||
tsconfig.json
|
||||
tsconfig.app.json
|
||||
tsconfig.test.json
|
||||
playwright.config.ts
|
||||
```
|
||||
|
||||
```json title="tsconfig.json"
|
||||
// This file just references two other configs.
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.test.json" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json title="tsconfig.app.json"
|
||||
{
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
// Configure TypeScript for the app here.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json title="tsconfig.test.json"
|
||||
{
|
||||
"include": ["./tests"],
|
||||
"compilerOptions": {
|
||||
// Configure TypeScript for tests here.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that `include` should be configured in each config to only apply to respective files.
|
||||
|
||||
### tsconfig path mapping
|
||||
|
||||
Playwright supports [path mapping](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) declared in the `tsconfig.json`. Make sure that `baseUrl` is also set.
|
||||
|
||||
Here is an example `tsconfig.json` that works with Playwright Test:
|
||||
Here is an example `tsconfig.json` that works with Playwright:
|
||||
|
||||
```json
|
||||
```json title="tsconfig.test.json"
|
||||
{
|
||||
"include": ["tests/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".", // This must be specified if "paths" is.
|
||||
"paths": {
|
||||
|
|
@ -74,23 +108,32 @@ test('example', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
### tsconfig resolution in Playwright
|
||||
|
||||
Before loading `playwright.config.ts`, Playwright will search for `tsconfig.json` file next to it and in parent directories up to the package root containing `package.json`. This `tsconfig.json` will be used to load `playwright.config.ts`.
|
||||
|
||||
Then, if you specify [`property: TestConfig.testDir`], and it contains a `tsconfig.json` file, Playwright will use it instead of the root `tsconfig.json`. This is **not recommended** and is left for backwards compatibility only. See above for the [recommended `references` setup](#tsconfigjson).
|
||||
|
||||
Playwright consults `include`, `exclude` and `files` properties of the `tsconfig.json` before loading any typescript file, either through `require` or `import`, to determine whether to apply `tsconfig` to this particular file.
|
||||
|
||||
## Manually compile tests with TypeScript
|
||||
|
||||
Sometimes, Playwright Test will not be able to transform your TypeScript code correctly, for example when you are using experimental or very recent features of TypeScript, usually configured in `tsconfig.json`.
|
||||
|
||||
In this case, you can perform your own TypeScript compilation before sending the tests to Playwright.
|
||||
|
||||
First add a `tsconfig.json` file inside the tests directory:
|
||||
First configure `tsconfig.test.json` to compile your tests:
|
||||
|
||||
```json
|
||||
```json title="tsconfig.test.json"
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": true,
|
||||
"outDir": "../tests-out",
|
||||
}
|
||||
"include": ["tests/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": true,
|
||||
"outDir": "./tests-out",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -99,7 +142,7 @@ In `package.json`, add two scripts:
|
|||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"pretest": "tsc --incremental -p tests/tsconfig.json",
|
||||
"pretest": "tsc --incremental -p tsconfig.test.json",
|
||||
"test": "playwright test -c tests-out"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import { getPackageJsonPath, mergeObjects } from '../util';
|
|||
import type { Matcher } from '../util';
|
||||
import type { ConfigCLIOverrides } from './ipc';
|
||||
import type { FullConfig, FullProject } from '../../types/test';
|
||||
import { setTransformConfig } from '../transform/transform';
|
||||
|
||||
export type ConfigLocation = {
|
||||
resolvedConfigFile?: string;
|
||||
|
|
@ -133,10 +132,6 @@ export class FullConfigInternal {
|
|||
this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, userConfig, this, p, this.configCLIOverrides, throwawayArtifactsPath));
|
||||
resolveProjectDependencies(this.projects);
|
||||
this._assignUniqueProjectIds(this.projects);
|
||||
setTransformConfig({
|
||||
babelPlugins: privateConfiguration?.babelPlugins || [],
|
||||
external: userConfig.build?.external || [],
|
||||
});
|
||||
this.config.projects = this.projects.map(p => p.project);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { gracefullyProcessExitDoNotHang, isRegExp } from 'playwright-core/lib/utils';
|
||||
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
|
||||
import { requireOrImport } from '../transform/transform';
|
||||
import { requireOrImport, setupTransformConfig } from '../transform/transform';
|
||||
import type { Config, Project } from '../../types/test';
|
||||
import { errorWithFile, fileIsModule } from '../util';
|
||||
import type { ConfigLocation } from './config';
|
||||
import { FullConfigInternal } from './config';
|
||||
import { addToCompilationCache } from '../transform/compilationCache';
|
||||
import { initializeEsmLoader, registerESMLoader } from './esmLoaderHost';
|
||||
import { configureESMLoader, registerESMLoader } from './esmLoaderHost';
|
||||
import { execArgvWithExperimentalLoaderOptions, execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
||||
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
|
||||
|
|
@ -88,9 +88,7 @@ export async function deserializeConfig(data: SerializedConfig): Promise<FullCon
|
|||
if (data.compilationCache)
|
||||
addToCompilationCache(data.compilationCache);
|
||||
|
||||
const config = await loadConfig(data.location, data.configCLIOverrides);
|
||||
await initializeEsmLoader();
|
||||
return config;
|
||||
return await loadConfig(data.location, data.configCLIOverrides);
|
||||
}
|
||||
|
||||
async function loadUserConfig(location: ConfigLocation): Promise<Config> {
|
||||
|
|
@ -101,8 +99,16 @@ async function loadUserConfig(location: ConfigLocation): Promise<Config> {
|
|||
}
|
||||
|
||||
export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLIOverrides, ignoreProjectDependencies = false): Promise<FullConfigInternal> {
|
||||
// 1. Look for tsconfig starting with `configDir` and going up to the package.json level.
|
||||
setupTransformConfig([], [], location.configDir, 'lookup-to-package-root');
|
||||
|
||||
// 2. Send tsconfig to ESM loader.
|
||||
await configureESMLoader();
|
||||
|
||||
// 3. Load and validate playwright config.
|
||||
const userConfig = await loadUserConfig(location);
|
||||
validateConfig(location.resolvedConfigFile || '<default config>', userConfig);
|
||||
|
||||
const fullConfig = new FullConfigInternal(location, userConfig, overrides || {});
|
||||
fullConfig.defineConfigWasUsed = !!(userConfig as any)[kDefineConfigWasUsed];
|
||||
if (ignoreProjectDependencies) {
|
||||
|
|
@ -111,6 +117,17 @@ export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLI
|
|||
project.teardown = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Load transform options from the playwright config.
|
||||
const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || [];
|
||||
const external = userConfig.build?.external || [];
|
||||
|
||||
// 5. When {config.testDir}/tsconfig.json is present, switch to it for backwards compatibility.
|
||||
setupTransformConfig(babelPlugins, external, fullConfig.config.rootDir, 'switch-if-present');
|
||||
|
||||
// 6. Sync new transform options to ESM loader.
|
||||
await configureESMLoader();
|
||||
|
||||
return fullConfig;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export async function incorporateCompilationCache() {
|
|||
addToCompilationCache(result.cache);
|
||||
}
|
||||
|
||||
export async function initializeEsmLoader() {
|
||||
export async function configureESMLoader() {
|
||||
if (!loaderChannel)
|
||||
return;
|
||||
await loaderChannel.send('setTransformConfig', { config: transformConfig() });
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { loadTestFile } from '../common/testLoader';
|
|||
import type { FullConfigInternal } from '../common/config';
|
||||
import { PoolBuilder } from '../common/poolBuilder';
|
||||
import { addToCompilationCache } from '../transform/compilationCache';
|
||||
import { incorporateCompilationCache, initializeEsmLoader } from '../common/esmLoaderHost';
|
||||
import { incorporateCompilationCache } from '../common/esmLoaderHost';
|
||||
|
||||
export class InProcessLoaderHost {
|
||||
private _config: FullConfigInternal;
|
||||
|
|
@ -34,7 +34,6 @@ export class InProcessLoaderHost {
|
|||
}
|
||||
|
||||
async start(errors: TestError[]) {
|
||||
await initializeEsmLoader();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,12 @@ interface TsConfig {
|
|||
paths?: { [key: string]: Array<string> };
|
||||
strict?: boolean;
|
||||
allowJs?: boolean;
|
||||
outDir?: string;
|
||||
};
|
||||
references?: { path: string }[];
|
||||
files?: string[];
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
}
|
||||
|
||||
export interface LoadedTsConfig {
|
||||
|
|
@ -50,14 +54,19 @@ export interface LoadedTsConfig {
|
|||
};
|
||||
absoluteBaseUrl?: string;
|
||||
allowJs?: boolean;
|
||||
files?: string[]; // absolute paths
|
||||
include?: string[]; // absolute path patterns
|
||||
exclude?: string[]; // absolute path patterns
|
||||
outDir?: string; // absolute path
|
||||
}
|
||||
|
||||
export interface TsConfigLoaderParams {
|
||||
cwd: string;
|
||||
searchUpToPackageRoot?: boolean;
|
||||
}
|
||||
|
||||
export function tsConfigLoader({ cwd, }: TsConfigLoaderParams): LoadedTsConfig[] {
|
||||
const configPath = resolveConfigPath(cwd);
|
||||
export function tsConfigLoader({ cwd, searchUpToPackageRoot }: TsConfigLoaderParams): LoadedTsConfig[] {
|
||||
const configPath = resolveConfigPath(cwd, searchUpToPackageRoot);
|
||||
|
||||
if (!configPath)
|
||||
return [];
|
||||
|
|
@ -67,27 +76,29 @@ export function tsConfigLoader({ cwd, }: TsConfigLoaderParams): LoadedTsConfig[]
|
|||
return [config, ...references];
|
||||
}
|
||||
|
||||
function resolveConfigPath(cwd: string): string | undefined {
|
||||
if (fs.statSync(cwd).isFile()) {
|
||||
return path.resolve(cwd);
|
||||
}
|
||||
|
||||
const configAbsolutePath = walkForTsConfig(cwd);
|
||||
function resolveConfigPath(cwd: string, searchUpToPackageRoot?: boolean): string | undefined {
|
||||
const configAbsolutePath = walkForTsConfig(cwd, searchUpToPackageRoot);
|
||||
return configAbsolutePath ? path.resolve(configAbsolutePath) : undefined;
|
||||
}
|
||||
|
||||
export function walkForTsConfig(
|
||||
function walkForTsConfig(
|
||||
directory: string,
|
||||
existsSync: (path: string) => boolean = fs.existsSync
|
||||
searchUpToPackageRoot?: boolean,
|
||||
): string | undefined {
|
||||
const tsconfigPath = path.join(directory, "./tsconfig.json");
|
||||
if (existsSync(tsconfigPath)) {
|
||||
if (fs.existsSync(tsconfigPath)) {
|
||||
return tsconfigPath;
|
||||
}
|
||||
const jsconfigPath = path.join(directory, "./jsconfig.json");
|
||||
if (existsSync(jsconfigPath)) {
|
||||
if (fs.existsSync(jsconfigPath)) {
|
||||
return jsconfigPath;
|
||||
}
|
||||
if (fs.existsSync(path.join(directory, 'package.json'))) {
|
||||
return undefined;
|
||||
}
|
||||
if (!searchUpToPackageRoot) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parentDirectory = path.join(directory, "../");
|
||||
|
||||
|
|
@ -96,7 +107,7 @@ export function walkForTsConfig(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
return walkForTsConfig(parentDirectory, existsSync);
|
||||
return walkForTsConfig(parentDirectory, searchUpToPackageRoot);
|
||||
}
|
||||
|
||||
function resolveConfigFile(baseConfigFile: string, referencedConfigFile: string) {
|
||||
|
|
@ -139,6 +150,7 @@ function loadTsConfig(
|
|||
Object.assign(result, base, { tsConfigPath: configFilePath });
|
||||
}
|
||||
|
||||
const configDir = path.dirname(configFilePath);
|
||||
if (parsedConfig.compilerOptions?.allowJs !== undefined)
|
||||
result.allowJs = parsedConfig.compilerOptions.allowJs;
|
||||
if (parsedConfig.compilerOptions?.paths !== undefined) {
|
||||
|
|
@ -148,14 +160,22 @@ function loadTsConfig(
|
|||
// https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/moduleSpecifiers.ts#L510
|
||||
result.paths = {
|
||||
mapping: parsedConfig.compilerOptions.paths,
|
||||
pathsBasePath: path.dirname(configFilePath),
|
||||
pathsBasePath: configDir,
|
||||
};
|
||||
}
|
||||
if (parsedConfig.compilerOptions?.baseUrl !== undefined) {
|
||||
// Follow tsc and resolve all relative file paths in the config right away.
|
||||
// This way it is safe to inherit paths between the configs.
|
||||
result.absoluteBaseUrl = path.resolve(path.dirname(configFilePath), parsedConfig.compilerOptions.baseUrl);
|
||||
result.absoluteBaseUrl = path.resolve(configDir, parsedConfig.compilerOptions.baseUrl);
|
||||
}
|
||||
if (parsedConfig.files)
|
||||
result.files = parsedConfig.files.map(file => path.resolve(configDir, file));
|
||||
if (parsedConfig.include)
|
||||
result.include = parsedConfig.include.map(pattern => path.resolve(configDir, pattern));
|
||||
if (parsedConfig.exclude)
|
||||
result.exclude = parsedConfig.exclude.map(pattern => path.resolve(configDir, pattern));
|
||||
if (parsedConfig.compilerOptions?.outDir)
|
||||
result.outDir = path.resolve(configDir, parsedConfig.compilerOptions.outDir);
|
||||
|
||||
for (const ref of parsedConfig.references || [])
|
||||
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
|
||||
|
|
|
|||
|
|
@ -26,28 +26,54 @@ import type { BabelPlugin, BabelTransformFunction } from './babelBundle';
|
|||
import { createFileMatcher, fileIsModule, resolveImportSpecifierExtension } from '../util';
|
||||
import type { Matcher } from '../util';
|
||||
import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules, installSourceMapSupportIfNeeded } from './compilationCache';
|
||||
import { debug, minimatch } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
const version = require('../../package.json').version;
|
||||
|
||||
type ParsedTsConfigData = {
|
||||
tsConfigPath: string;
|
||||
pathsBase?: string;
|
||||
paths: { key: string, values: string[] }[];
|
||||
allowJs: boolean;
|
||||
files?: string[];
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
};
|
||||
const cachedTSConfigs = new Map<string, ParsedTsConfigData[]>();
|
||||
|
||||
export type TransformConfig = {
|
||||
type TransformConfig = {
|
||||
babelPlugins: [string, any?][];
|
||||
external: string[];
|
||||
tsconfigs: ParsedTsConfigData[];
|
||||
serializedTsconfigs: string;
|
||||
};
|
||||
|
||||
let _transformConfig: TransformConfig = {
|
||||
babelPlugins: [],
|
||||
external: [],
|
||||
tsconfigs: [],
|
||||
serializedTsconfigs: JSON.stringify([]),
|
||||
};
|
||||
|
||||
let _externalMatcher: Matcher = () => false;
|
||||
|
||||
export function setupTransformConfig(babelPlugins: [string, any?][], external: string[], tsconfigDir: string, tsconfigBehavior: 'lookup-to-package-root' | 'switch-if-present') {
|
||||
let tsconfigs = tsConfigLoader({
|
||||
cwd: tsconfigDir,
|
||||
searchUpToPackageRoot: tsconfigBehavior === 'lookup-to-package-root',
|
||||
}).map(validateTsConfig);
|
||||
if (tsconfigBehavior === 'switch-if-present' && !tsconfigs.length) {
|
||||
// Keep existing tsconfig if the new one was not found.
|
||||
tsconfigs = _transformConfig.tsconfigs;
|
||||
}
|
||||
if (tsconfigs.length)
|
||||
debug('pw:test:transform')(`${tsconfigBehavior === 'switch-if-present' ? 'switching to' : 'using'} ${tsconfigs[0].tsConfigPath}`);
|
||||
setTransformConfig({
|
||||
babelPlugins,
|
||||
external,
|
||||
tsconfigs,
|
||||
serializedTsconfigs: JSON.stringify(tsconfigs),
|
||||
});
|
||||
}
|
||||
|
||||
export function setTransformConfig(config: TransformConfig) {
|
||||
_transformConfig = config;
|
||||
_externalMatcher = createFileMatcher(_transformConfig.external);
|
||||
|
|
@ -63,20 +89,65 @@ function validateTsConfig(tsconfig: LoadedTsConfig): ParsedTsConfigData {
|
|||
const pathsBase = tsconfig.absoluteBaseUrl ?? tsconfig.paths?.pathsBasePath;
|
||||
// Only add the catch-all mapping when baseUrl is specified
|
||||
const pathsFallback = tsconfig.absoluteBaseUrl ? [{ key: '*', values: ['*'] }] : [];
|
||||
// https://www.typescriptlang.org/tsconfig#exclude
|
||||
const defaultExclude = ['node_modules', 'bower_components', 'jspm_packages'];
|
||||
if (tsconfig.outDir)
|
||||
defaultExclude.push(tsconfig.outDir);
|
||||
// Default "include" is **/*, unless "files" are specified.
|
||||
// https://www.typescriptlang.org/tsconfig#include
|
||||
const defaultInclude = tsconfig.files ? [] : ['**/*'];
|
||||
return {
|
||||
tsConfigPath: tsconfig.tsConfigPath,
|
||||
allowJs: !!tsconfig.allowJs,
|
||||
files: tsconfig.files,
|
||||
include: tsconfig.include ?? defaultInclude,
|
||||
exclude: tsconfig.exclude ?? defaultExclude,
|
||||
pathsBase,
|
||||
paths: Object.entries(tsconfig.paths?.mapping || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
|
||||
};
|
||||
}
|
||||
|
||||
function loadAndValidateTsconfigsForFile(file: string): ParsedTsConfigData[] {
|
||||
const cwd = path.dirname(file);
|
||||
if (!cachedTSConfigs.has(cwd)) {
|
||||
const loaded = tsConfigLoader({ cwd });
|
||||
cachedTSConfigs.set(cwd, loaded.map(validateTsConfig));
|
||||
}
|
||||
return cachedTSConfigs.get(cwd)!;
|
||||
function createTsconfigMatcher(tsconfig: ParsedTsConfigData): Matcher {
|
||||
const include = tsconfig.include.map(makeMinimatcher);
|
||||
const exclude = tsconfig.exclude.map(makeMinimatcher);
|
||||
return (filename: string) => {
|
||||
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
|
||||
// Explicitly mentioning a file in "files" overrides "allowJs" setting.
|
||||
if (tsconfig.files && tsconfig.files.includes(filename))
|
||||
return true;
|
||||
// On the other hand, "allowJs" overrides "includes".
|
||||
if (!isTypeScript && !tsconfig.allowJs)
|
||||
return false;
|
||||
return include.some(matcher => matcher(filename)) && !exclude.some(matcher => matcher(filename));
|
||||
};
|
||||
}
|
||||
|
||||
function makeMinimatcher(pattern: string): Matcher {
|
||||
// Note: this is an approximation of what tsc does.
|
||||
// https://github.com/microsoft/TypeScript/blob/3a0869fd97ea38d2eedcda48d4c794487c345c42/src/compiler/utilities.ts#L9327
|
||||
//
|
||||
// From https://www.typescriptlang.org/tsconfig#include:
|
||||
// If the last path segment in a pattern does not contain a file extension or wildcard character,
|
||||
// then it is treated as a directory, and files with supported extensions inside that directory
|
||||
// are included (e.g. .ts, .tsx, and .d.ts by default, with .js and .jsx if allowJs is set to true).
|
||||
// We just check allowJs later, so can list all the extensions here.
|
||||
const [lastPart] = pattern.replace(/\\/g, '/').split('/').reverse();
|
||||
if (!lastPart.includes('.') && !lastPart.includes('?') && !lastPart.includes('*'))
|
||||
pattern = path.join(pattern, '**', '*@(.ts|.tsx|.js|.jsx)');
|
||||
const m = new minimatch.Minimatch(pattern, { dot: true, nocase: true });
|
||||
return (filename: string) => m.match(filename);
|
||||
}
|
||||
|
||||
const cachedTsconfigMatcher = new Map<ParsedTsConfigData, Matcher>();
|
||||
function applicableTsconfigs(filename: string): ParsedTsConfigData[] {
|
||||
return _transformConfig.tsconfigs.filter(tsconfig => {
|
||||
let matcher = cachedTsconfigMatcher.get(tsconfig);
|
||||
if (!matcher) {
|
||||
matcher = createTsconfigMatcher(tsconfig);
|
||||
cachedTsconfigMatcher.set(tsconfig, matcher);
|
||||
}
|
||||
return matcher(filename);
|
||||
});
|
||||
}
|
||||
|
||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||
|
|
@ -91,11 +162,7 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
|||
if (isRelativeSpecifier(specifier))
|
||||
return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier));
|
||||
|
||||
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
|
||||
const tsconfigs = loadAndValidateTsconfigsForFile(filename);
|
||||
for (const tsconfig of tsconfigs) {
|
||||
if (!isTypeScript && !tsconfig.allowJs)
|
||||
continue;
|
||||
for (const tsconfig of applicableTsconfigs(filename)) {
|
||||
let longestPrefixLength = -1;
|
||||
let pathMatchedByLongestPrefix: string | undefined;
|
||||
|
||||
|
|
@ -196,6 +263,7 @@ function calculateHash(content: string, filePath: string, isModule: boolean, plu
|
|||
.update(version)
|
||||
.update(pluginsPrologue.map(p => p[0]).join(','))
|
||||
.update(pluginsEpilogue.map(p => p[0]).join(','))
|
||||
.update(_transformConfig.serializedTsconfigs)
|
||||
.digest('hex');
|
||||
return hash;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ for (const dir of fs.readdirSync(__dirname)) {
|
|||
test(project, async () => {
|
||||
await run('npx', ['playwright', 'test', '--project=' + project, '--reporter=list'], folder);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,8 +105,9 @@ test('should respect path resolver in experimental mode', async ({ runInlineTest
|
|||
const result = await runInlineTest({
|
||||
'package.json': JSON.stringify({ type: 'module' }),
|
||||
'playwright.config.ts': `
|
||||
import { foo } from 'util/b.js';
|
||||
export default {
|
||||
projects: [{name: 'foo'}],
|
||||
projects: [{ name: foo }],
|
||||
};
|
||||
`,
|
||||
'tsconfig.json': `{
|
||||
|
|
@ -124,7 +125,8 @@ test('should respect path resolver in experimental mode', async ({ runInlineTest
|
|||
import { foo } from 'util/b.js';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('check project name', ({}, testInfo) => {
|
||||
expect(testInfo.project.name).toBe(foo);
|
||||
expect(testInfo.project.name).toBe('foo');
|
||||
expect(foo).toBe('foo');
|
||||
});
|
||||
`,
|
||||
'foo/bar/util/b.ts': `
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ test('should respect path resolver', async ({ runInlineTest }) => {
|
|||
'foo/bar/util/b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`,
|
||||
'foo/bar/wrong-util/b.ts': `
|
||||
export const foo: string = 'wrong';
|
||||
`,
|
||||
'helper.ts': `
|
||||
export { foo } from 'util3';
|
||||
`,
|
||||
|
|
@ -72,13 +75,13 @@ test('should respect path resolver', async ({ runInlineTest }) => {
|
|||
"lib": ["esnext", "dom", "DOM.Iterable"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"parent-util/*": ["../foo/bar/util/*"],
|
||||
"util/*": ["../foo/bar/wrong-util/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'dir/inner.spec.ts': `
|
||||
// This import should pick up <root>/dir/tsconfig
|
||||
import { foo } from 'parent-util/b';
|
||||
// Note: <root>/dir/tsconfig should be ignored, <root>/tsconfig should be used instead
|
||||
import { foo } from 'util/b';
|
||||
// This import should pick up <root>/tsconfig through the helper
|
||||
import { foo as foo2 } from '../helper';
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
@ -141,6 +144,9 @@ test('should respect baseurl w/o paths', async ({ runInlineTest }) => {
|
|||
'foo/bar/util/b.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
export default { testDir: 'dir2' };
|
||||
`,
|
||||
'dir2/tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
|
|
@ -172,6 +178,9 @@ test('should fallback to *:* when baseurl and paths are specified', async ({ run
|
|||
'shared/x.ts': `
|
||||
export const x = 43;
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
export default { testDir: 'dir2' };
|
||||
`,
|
||||
'dir2/tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
|
|
@ -206,6 +215,9 @@ test('should use the location of the tsconfig as the paths root when no baseUrl
|
|||
'foo/bar/util/b.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'playwright.config.ts': `
|
||||
export default { testDir: 'dir2' };
|
||||
`,
|
||||
'dir2/tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
|
|
@ -366,6 +378,9 @@ test('should not use baseurl for relative imports when dir with same name exists
|
|||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/15891' });
|
||||
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
export default { testDir: 'frontend' };
|
||||
`,
|
||||
'frontend/tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
|
|
@ -641,3 +656,244 @@ test('should respect tsconfig project references', async ({ runInlineTest }) =>
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
||||
test('should respect files property with allowJs', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"paths": {
|
||||
"~/*": ["./mapped/*"],
|
||||
},
|
||||
},
|
||||
"files": ["example.spec.js"],
|
||||
}`,
|
||||
'example.spec.js': `
|
||||
import { foo } from '~/helper';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'mapped/helper.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.output).not.toContain(`Could not`);
|
||||
});
|
||||
|
||||
test('should respect files property', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"paths": {
|
||||
"~/*": ["./mapped/*"],
|
||||
},
|
||||
},
|
||||
"files": ["one.spec.ts"],
|
||||
}`,
|
||||
'one.spec.ts': `
|
||||
import { foo } from '~/helper1';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'two.spec.ts': `
|
||||
import { foo } from '~/helper2';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'mapped/helper1.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'mapped/helper2.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).not.toContain(`Cannot find module '~/helper1'`);
|
||||
expect(result.output).toContain(`Cannot find module '~/helper2'`);
|
||||
});
|
||||
|
||||
test('should respect include property', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"paths": {
|
||||
"~/*": ["./mapped/*"],
|
||||
},
|
||||
},
|
||||
"include": ["**/one*"],
|
||||
}`,
|
||||
'one.spec.ts': `
|
||||
import { foo } from '~/helper1';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'two.spec.ts': `
|
||||
import { foo } from '~/helper2';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'mapped/helper1.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'mapped/helper2.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).not.toContain(`Cannot find module '~/helper1'`);
|
||||
expect(result.output).toContain(`Cannot find module '~/helper2'`);
|
||||
});
|
||||
|
||||
test('should respect exclude property', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"paths": {
|
||||
"~/*": ["./mapped/*"],
|
||||
},
|
||||
},
|
||||
"exclude": ["**/two*"],
|
||||
}`,
|
||||
'one.spec.ts': `
|
||||
import { foo } from '~/helper1';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'two.spec.ts': `
|
||||
import { foo } from '~/helper2';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
'mapped/helper1.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'mapped/helper2.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.output).not.toContain(`Cannot find module '~/helper1'`);
|
||||
expect(result.output).toContain(`Cannot find module '~/helper2'`);
|
||||
});
|
||||
|
||||
test('should use root tsconfig for playwright.config and switch', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
import { foo } from '~/foo';
|
||||
export default {
|
||||
testDir: './tests' + foo,
|
||||
};
|
||||
`,
|
||||
'tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./mapped-from-root/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'mapped-from-root/foo.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'tests42/tsconfig.json': `{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["../mapped-from-tests/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'tests42/a.test.ts': `
|
||||
import { foo } from '~/foo';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}) => {
|
||||
expect(foo).toBe(43);
|
||||
});
|
||||
`,
|
||||
'mapped-from-tests/foo.ts': `
|
||||
export const foo = 43;
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).not.toContain(`Could not`);
|
||||
});
|
||||
|
||||
test('should work in the recommended setup', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
export default {
|
||||
testDir: './tests',
|
||||
};
|
||||
`,
|
||||
'tsconfig.json': `{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.test.json" }
|
||||
]
|
||||
}`,
|
||||
'tsconfig.app.json': `{
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@helpers/*": ["./src/helpers/*"],
|
||||
},
|
||||
}
|
||||
}`,
|
||||
'tsconfig.test.json': `{
|
||||
"include": ["./tests"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
},
|
||||
}
|
||||
}`,
|
||||
'src/helpers/foo.ts': `
|
||||
export const foo = 42;
|
||||
`,
|
||||
'src/source.ts': `
|
||||
export { foo } from '@helpers/foo';
|
||||
`,
|
||||
'tests/a.spec.ts': `
|
||||
import { foo } from '~/source';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}) => {
|
||||
expect(foo).toBe(42);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).not.toContain(`Could not`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue