From 4eb6fca149dd94ecf3128b8a3961ce3715c25b0a Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 29 Mar 2022 14:46:25 -0700 Subject: [PATCH] cherry-pick(#13137): fix(esm): make sure import from './foo.js' is supported (#13160) Drive-by: migrate all @esm tests to esm.spec.ts. --- .github/workflows/tests_primary.yml | 2 +- packages/playwright-test/src/transform.ts | 40 ++++--- tests/playwright-test/esm.spec.ts | 126 ++++++++++++++++++++++ tests/playwright-test/loader.spec.ts | 44 -------- tests/playwright-test/resolver.spec.ts | 38 ------- 5 files changed, 151 insertions(+), 99 deletions(-) create mode 100644 tests/playwright-test/esm.spec.ts diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index b18b7716bdea5..7dd214b241a21 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -93,7 +93,7 @@ jobs: DEBUG: pw:install - run: npm run build - run: npx playwright install --with-deps - - run: npm run ttest -- --grep=@esm + - run: npm run ttest -- esm.spec.ts test_html_report: name: HTML Report diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index e95b3fe995a51..569cd8407d359 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -99,25 +99,33 @@ export function resolveHook(filename: string, specifier: string): string | undef if (!isTypeScript) return; const tsconfig = loadAndValidateTsconfigForFile(filename); - if (!tsconfig) - return; - 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; - 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); - 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 (tsconfig) { + 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; + 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); + 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 (specifier.endsWith('.js')) { + const resolved = path.resolve(path.dirname(filename), specifier); + if (resolved.endsWith('.js')) { + const tsResolved = resolved.substring(0, resolved.length - 3) + '.ts'; + if (!fs.existsSync(resolved) && fs.existsSync(tsResolved)) + return tsResolved; + } + } } export function transformHook(code: string, filename: string, isModule = false): string { diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts new file mode 100644 index 0000000000000..d8b28bf1325f3 --- /dev/null +++ b/tests/playwright-test/esm.spec.ts @@ -0,0 +1,126 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +// Note: tests from this file are additionally run on Node16 bots. + +test('should load nested as esm when package.json has type module', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + //@no-header + import * as fs from 'fs'; + export default { projects: [{name: 'foo'}] }; + `, + 'package.json': JSON.stringify({ type: 'module' }), + 'nested/folder/a.esm.test.js': ` + const { test } = pwt; + test('check project name', ({}, testInfo) => { + expect(testInfo.project.name).toBe('foo'); + }); + ` + }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + +test('should import esm from ts when package.json has type module in experimental mode', async ({ runInlineTest }) => { + // We only support experimental esm mode on Node 16+ + test.skip(parseInt(process.version.slice(1), 10) < 16); + const result = await runInlineTest({ + 'playwright.config.ts': ` + import * as fs from 'fs'; + export default { projects: [{name: 'foo'}] }; + `, + 'package.json': JSON.stringify({ type: 'module' }), + 'a.test.ts': ` + import { foo } from './b.ts'; + import { bar } from './c.js'; + import { qux } from './d.js'; + const { test } = pwt; + test('check project name', ({}, testInfo) => { + expect(testInfo.project.name).toBe('foo'); + expect(bar).toBe('bar'); + expect(qux).toBe('qux'); + }); + `, + 'b.ts': ` + export const foo: string = 'foo'; + `, + 'c.ts': ` + export const bar: string = 'bar'; + `, + 'd.js': ` + //@no-header + export const qux = 'qux'; + `, + }, {}, { PW_EXPERIMENTAL_TS_ESM: true }); + + expect(result.exitCode).toBe(0); +}); + +test('should propagate subprocess exit code in experimental mode', async ({ runInlineTest }) => { + // We only support experimental esm mode on Node 16+ + test.skip(parseInt(process.version.slice(1), 10) < 16); + const result = await runInlineTest({ + 'package.json': JSON.stringify({ type: 'module' }), + 'a.test.ts': ` + const { test } = pwt; + test('failing test', ({}, testInfo) => { + expect(1).toBe(2); + }); + `, + }, {}, { PW_EXPERIMENTAL_TS_ESM: true }); + + expect(result.exitCode).toBe(1); +}); + +test('should respect path resolver in experimental mode', async ({ runInlineTest }) => { + // We only support experimental esm mode on Node 16+ + test.skip(parseInt(process.version.slice(1), 10) < 16); + const result = await runInlineTest({ + 'package.json': JSON.stringify({ type: 'module' }), + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": ".", + "paths": { + "util/*": ["./foo/bar/util/*"], + }, + }, + }`, + 'a.test.ts': ` + import { foo } from 'util/b.ts'; + const { test } = pwt; + test('check project name', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/bar/util/b.ts': ` + export const foo: string = 'foo'; + `, + }, {}, { PW_EXPERIMENTAL_TS_ESM: true }); + + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/loader.spec.ts b/tests/playwright-test/loader.spec.ts index 6aec88c476a12..a3950fae0be0d 100644 --- a/tests/playwright-test/loader.spec.ts +++ b/tests/playwright-test/loader.spec.ts @@ -241,50 +241,6 @@ test('should fail to load ts from esm when package.json has type module', async expect(result.output).toContain('Cannot import a typescript file from an esmodule'); }); -test('should import esm from ts when package.json has type module in experimental mode @esm', async ({ runInlineTest }) => { - // We only support experimental esm mode on Node 16+ - test.skip(parseInt(process.version.slice(1), 10) < 16); - const result = await runInlineTest({ - 'playwright.config.ts': ` - import * as fs from 'fs'; - export default { projects: [{name: 'foo'}] }; - `, - 'package.json': JSON.stringify({ type: 'module' }), - 'a.test.ts': ` - import { foo } from './b.ts'; - const { test } = pwt; - test('check project name', ({}, testInfo) => { - expect(testInfo.project.name).toBe('foo'); - }); - `, - 'b.ts': ` - export const foo: string = 'foo'; - ` - }, {}, { - PW_EXPERIMENTAL_TS_ESM: true - }); - - expect(result.exitCode).toBe(0); -}); - -test('should propagate subprocess exit code in experimental mode @esm', async ({ runInlineTest }) => { - // We only support experimental esm mode on Node 16+ - test.skip(parseInt(process.version.slice(1), 10) < 16); - const result = await runInlineTest({ - 'package.json': JSON.stringify({ type: 'module' }), - 'a.test.ts': ` - const { test } = pwt; - test('failing test', ({}, testInfo) => { - expect(1).toBe(2); - }); - `, - }, {}, { - PW_EXPERIMENTAL_TS_ESM: true - }); - - expect(result.exitCode).toBe(1); -}); - test('should filter stack trace for simple expect', async ({ runInlineTest }) => { const result = await runInlineTest({ 'expect-test.spec.ts': ` diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index 61a66a9d3e422..6d36cd5191ef8 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -163,41 +163,3 @@ test('should respect baseurl w/o paths', async ({ runInlineTest }) => { expect(result.passed).toBe(1); expect(result.output).not.toContain(`Could not`); }); - -test('should respect path resolver in experimental mode @esm', async ({ runInlineTest }) => { - // We only support experimental esm mode on Node 16+ - test.skip(parseInt(process.version.slice(1), 10) < 16); - const result = await runInlineTest({ - 'package.json': JSON.stringify({ type: 'module' }), - 'playwright.config.ts': ` - export default { - projects: [{name: 'foo'}], - }; - `, - 'tsconfig.json': `{ - "compilerOptions": { - "target": "ES2019", - "module": "commonjs", - "lib": ["esnext", "dom", "DOM.Iterable"], - "baseUrl": ".", - "paths": { - "util/*": ["./foo/bar/util/*"], - }, - }, - }`, - 'a.test.ts': ` - import { foo } from 'util/b.ts'; - const { test } = pwt; - test('check project name', ({}, testInfo) => { - expect(testInfo.project.name).toBe(foo); - }); - `, - 'foo/bar/util/b.ts': ` - export const foo: string = 'foo'; - `, - }, {}, { - PW_EXPERIMENTAL_TS_ESM: true - }); - - expect(result.exitCode).toBe(0); -});