diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index 7273fcb178..1d273fc796 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -100,23 +100,52 @@ export function resolveHook(filename: string, specifier: string): string | undef return; const tsconfig = loadAndValidateTsconfigForFile(filename); if (tsconfig) { + let longestPrefixLength = -1; + let pathMatchedByLongestPrefix: string | undefined; + for (const { key, values } of tsconfig.paths) { - const keyHasStar = key[key.length - 1] === '*'; - const matches = specifier.startsWith(keyHasStar ? key.substring(0, key.length - 1) : key); - if (!matches) - continue; + let matchedPartOfSpecifier = specifier; + + const [keyPrefix, keySuffix] = key.split('*'); + if (key.includes('*')) { + // * If pattern contains '*' then to match pattern "*" module name must start with the and end with . + // * denotes part of the module name between and . + // * If module name can be matches with multiple patterns then pattern with the longest prefix will be picked. + // https://github.com/microsoft/TypeScript/blob/f82d0cb3299c04093e3835bc7e29f5b40475f586/src/compiler/moduleNameResolver.ts#L1049 + if (keyPrefix) { + if (!specifier.startsWith(keyPrefix)) + continue; + matchedPartOfSpecifier = matchedPartOfSpecifier.substring(keyPrefix.length, matchedPartOfSpecifier.length); + } + if (keySuffix) { + if (!specifier.endsWith(keySuffix)) + continue; + matchedPartOfSpecifier = matchedPartOfSpecifier.substring(0, matchedPartOfSpecifier.length - keySuffix.length); + } + } else { + if (specifier !== key) + continue; + matchedPartOfSpecifier = specifier; + } + for (const value of values) { - const valueHasStar = value[value.length - 1] === '*'; - let candidate = valueHasStar ? value.substring(0, value.length - 1) : value; - if (valueHasStar && keyHasStar) - candidate += specifier.substring(key.length - 1); + let candidate: string = value; + + if (value.includes('*')) + candidate = candidate.replace('*', matchedPartOfSpecifier); candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep)); for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']) { - if (fs.existsSync(candidate + ext)) - return candidate; + if (fs.existsSync(candidate + ext)) { + if (keyPrefix.length > longestPrefixLength) { + longestPrefixLength = keyPrefix.length; + pathMatchedByLongestPrefix = candidate; + } + } } } } + if (pathMatchedByLongestPrefix) + return pathMatchedByLongestPrefix; } if (specifier.endsWith('.js')) { const resolved = path.resolve(path.dirname(filename), specifier); diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index 6d36cd5191..98a33aa4cc 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -163,3 +163,110 @@ test('should respect baseurl w/o paths', async ({ runInlineTest }) => { expect(result.passed).toBe(1); expect(result.output).not.toContain(`Could not`); }); + +test('should respect complex path resolver', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": ".", + "paths": { + "prefix-*": ["./prefix-*/bar"], + "prefix-*-suffix": ["./prefix-*-suffix/bar"], + "*-suffix": ["./*-suffix/bar"], + "no-star": ["./no-star-foo"], + "longest-*": ["./this-is-not-the-longest-prefix"], + "longest-pre*": ["./this-is-the-longest-prefix"], + "*bar": ["./*bar"], + "*[bar]": ["*foo"], + }, + }, + }`, + 'a.spec.ts': ` + import { foo } from 'prefix-matchedstar'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'prefix-matchedstar/bar/index.ts': ` + export const foo: string = 'foo'; + `, + 'b.spec.ts': ` + import { foo } from 'prefix-matchedstar-suffix'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'prefix-matchedstar-suffix/bar.ts': ` + export const foo: string = 'foo'; + `, + 'c.spec.ts': ` + import { foo } from 'matchedstar-suffix'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'matchedstar-suffix/bar.ts': ` + export const foo: string = 'foo'; + `, + 'd.spec.ts': ` + import { foo } from 'no-star'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + './no-star-foo.ts': ` + export const foo: string = 'foo'; + `, + 'e.spec.ts': ` + import { foo } from 'longest-prefix'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + './this-is-the-longest-prefix.ts': ` + // this module should be resolved as it matches by a longer prefix + export const foo: string = 'foo'; + `, + './this-is-not-the-longest-prefix.ts': ` + // This module should't be resolved as it matches by a shorter prefix + export const bar: string = 'bar'; + `, + 'f.spec.ts': ` + import { foo } from 'barfoobar'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'barfoobar.ts': ` + export const foo: string = 'foo'; + `, + 'g.spec.ts': ` + import { foo } from 'foo/[bar]'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/foo.ts': ` + export const foo: string = 'foo'; + `, + }); + + expect(result.passed).toBe(7); + expect(result.exitCode).toBe(0); + expect(result.output).not.toContain(`Could not`); +});