Skip to content

Commit 4eb6fca

Browse files
authored
cherry-pick(#13137): fix(esm): make sure import from './foo.js' is supported (#13160)
Drive-by: migrate all @esm tests to esm.spec.ts.
1 parent e55a3ff commit 4eb6fca

5 files changed

Lines changed: 151 additions & 99 deletions

File tree

.github/workflows/tests_primary.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
DEBUG: pw:install
9494
- run: npm run build
9595
- run: npx playwright install --with-deps
96-
- run: npm run ttest -- --grep=@esm
96+
- run: npm run ttest -- esm.spec.ts
9797

9898
test_html_report:
9999
name: HTML Report

packages/playwright-test/src/transform.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,33 @@ export function resolveHook(filename: string, specifier: string): string | undef
9999
if (!isTypeScript)
100100
return;
101101
const tsconfig = loadAndValidateTsconfigForFile(filename);
102-
if (!tsconfig)
103-
return;
104-
for (const { key, values } of tsconfig.paths) {
105-
const keyHasStar = key[key.length - 1] === '*';
106-
const matches = specifier.startsWith(keyHasStar ? key.substring(0, key.length - 1) : key);
107-
if (!matches)
108-
continue;
109-
for (const value of values) {
110-
const valueHasStar = value[value.length - 1] === '*';
111-
let candidate = valueHasStar ? value.substring(0, value.length - 1) : value;
112-
if (valueHasStar && keyHasStar)
113-
candidate += specifier.substring(key.length - 1);
114-
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
115-
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']) {
116-
if (fs.existsSync(candidate + ext))
117-
return candidate;
102+
if (tsconfig) {
103+
for (const { key, values } of tsconfig.paths) {
104+
const keyHasStar = key[key.length - 1] === '*';
105+
const matches = specifier.startsWith(keyHasStar ? key.substring(0, key.length - 1) : key);
106+
if (!matches)
107+
continue;
108+
for (const value of values) {
109+
const valueHasStar = value[value.length - 1] === '*';
110+
let candidate = valueHasStar ? value.substring(0, value.length - 1) : value;
111+
if (valueHasStar && keyHasStar)
112+
candidate += specifier.substring(key.length - 1);
113+
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep));
114+
for (const ext of ['', '.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']) {
115+
if (fs.existsSync(candidate + ext))
116+
return candidate;
117+
}
118118
}
119119
}
120120
}
121+
if (specifier.endsWith('.js')) {
122+
const resolved = path.resolve(path.dirname(filename), specifier);
123+
if (resolved.endsWith('.js')) {
124+
const tsResolved = resolved.substring(0, resolved.length - 3) + '.ts';
125+
if (!fs.existsSync(resolved) && fs.existsSync(tsResolved))
126+
return tsResolved;
127+
}
128+
}
121129
}
122130

123131
export function transformHook(code: string, filename: string, isModule = false): string {

tests/playwright-test/esm.spec.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './playwright-test-fixtures';
18+
19+
// Note: tests from this file are additionally run on Node16 bots.
20+
21+
test('should load nested as esm when package.json has type module', async ({ runInlineTest }) => {
22+
const result = await runInlineTest({
23+
'playwright.config.js': `
24+
//@no-header
25+
import * as fs from 'fs';
26+
export default { projects: [{name: 'foo'}] };
27+
`,
28+
'package.json': JSON.stringify({ type: 'module' }),
29+
'nested/folder/a.esm.test.js': `
30+
const { test } = pwt;
31+
test('check project name', ({}, testInfo) => {
32+
expect(testInfo.project.name).toBe('foo');
33+
});
34+
`
35+
});
36+
37+
expect(result.exitCode).toBe(0);
38+
expect(result.passed).toBe(1);
39+
});
40+
41+
test('should import esm from ts when package.json has type module in experimental mode', async ({ runInlineTest }) => {
42+
// We only support experimental esm mode on Node 16+
43+
test.skip(parseInt(process.version.slice(1), 10) < 16);
44+
const result = await runInlineTest({
45+
'playwright.config.ts': `
46+
import * as fs from 'fs';
47+
export default { projects: [{name: 'foo'}] };
48+
`,
49+
'package.json': JSON.stringify({ type: 'module' }),
50+
'a.test.ts': `
51+
import { foo } from './b.ts';
52+
import { bar } from './c.js';
53+
import { qux } from './d.js';
54+
const { test } = pwt;
55+
test('check project name', ({}, testInfo) => {
56+
expect(testInfo.project.name).toBe('foo');
57+
expect(bar).toBe('bar');
58+
expect(qux).toBe('qux');
59+
});
60+
`,
61+
'b.ts': `
62+
export const foo: string = 'foo';
63+
`,
64+
'c.ts': `
65+
export const bar: string = 'bar';
66+
`,
67+
'd.js': `
68+
//@no-header
69+
export const qux = 'qux';
70+
`,
71+
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
72+
73+
expect(result.exitCode).toBe(0);
74+
});
75+
76+
test('should propagate subprocess exit code in experimental mode', async ({ runInlineTest }) => {
77+
// We only support experimental esm mode on Node 16+
78+
test.skip(parseInt(process.version.slice(1), 10) < 16);
79+
const result = await runInlineTest({
80+
'package.json': JSON.stringify({ type: 'module' }),
81+
'a.test.ts': `
82+
const { test } = pwt;
83+
test('failing test', ({}, testInfo) => {
84+
expect(1).toBe(2);
85+
});
86+
`,
87+
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
88+
89+
expect(result.exitCode).toBe(1);
90+
});
91+
92+
test('should respect path resolver in experimental mode', async ({ runInlineTest }) => {
93+
// We only support experimental esm mode on Node 16+
94+
test.skip(parseInt(process.version.slice(1), 10) < 16);
95+
const result = await runInlineTest({
96+
'package.json': JSON.stringify({ type: 'module' }),
97+
'playwright.config.ts': `
98+
export default {
99+
projects: [{name: 'foo'}],
100+
};
101+
`,
102+
'tsconfig.json': `{
103+
"compilerOptions": {
104+
"target": "ES2019",
105+
"module": "commonjs",
106+
"lib": ["esnext", "dom", "DOM.Iterable"],
107+
"baseUrl": ".",
108+
"paths": {
109+
"util/*": ["./foo/bar/util/*"],
110+
},
111+
},
112+
}`,
113+
'a.test.ts': `
114+
import { foo } from 'util/b.ts';
115+
const { test } = pwt;
116+
test('check project name', ({}, testInfo) => {
117+
expect(testInfo.project.name).toBe(foo);
118+
});
119+
`,
120+
'foo/bar/util/b.ts': `
121+
export const foo: string = 'foo';
122+
`,
123+
}, {}, { PW_EXPERIMENTAL_TS_ESM: true });
124+
125+
expect(result.exitCode).toBe(0);
126+
});

tests/playwright-test/loader.spec.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -241,50 +241,6 @@ test('should fail to load ts from esm when package.json has type module', async
241241
expect(result.output).toContain('Cannot import a typescript file from an esmodule');
242242
});
243243

244-
test('should import esm from ts when package.json has type module in experimental mode @esm', async ({ runInlineTest }) => {
245-
// We only support experimental esm mode on Node 16+
246-
test.skip(parseInt(process.version.slice(1), 10) < 16);
247-
const result = await runInlineTest({
248-
'playwright.config.ts': `
249-
import * as fs from 'fs';
250-
export default { projects: [{name: 'foo'}] };
251-
`,
252-
'package.json': JSON.stringify({ type: 'module' }),
253-
'a.test.ts': `
254-
import { foo } from './b.ts';
255-
const { test } = pwt;
256-
test('check project name', ({}, testInfo) => {
257-
expect(testInfo.project.name).toBe('foo');
258-
});
259-
`,
260-
'b.ts': `
261-
export const foo: string = 'foo';
262-
`
263-
}, {}, {
264-
PW_EXPERIMENTAL_TS_ESM: true
265-
});
266-
267-
expect(result.exitCode).toBe(0);
268-
});
269-
270-
test('should propagate subprocess exit code in experimental mode @esm', async ({ runInlineTest }) => {
271-
// We only support experimental esm mode on Node 16+
272-
test.skip(parseInt(process.version.slice(1), 10) < 16);
273-
const result = await runInlineTest({
274-
'package.json': JSON.stringify({ type: 'module' }),
275-
'a.test.ts': `
276-
const { test } = pwt;
277-
test('failing test', ({}, testInfo) => {
278-
expect(1).toBe(2);
279-
});
280-
`,
281-
}, {}, {
282-
PW_EXPERIMENTAL_TS_ESM: true
283-
});
284-
285-
expect(result.exitCode).toBe(1);
286-
});
287-
288244
test('should filter stack trace for simple expect', async ({ runInlineTest }) => {
289245
const result = await runInlineTest({
290246
'expect-test.spec.ts': `

tests/playwright-test/resolver.spec.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -163,41 +163,3 @@ test('should respect baseurl w/o paths', async ({ runInlineTest }) => {
163163
expect(result.passed).toBe(1);
164164
expect(result.output).not.toContain(`Could not`);
165165
});
166-
167-
test('should respect path resolver in experimental mode @esm', async ({ runInlineTest }) => {
168-
// We only support experimental esm mode on Node 16+
169-
test.skip(parseInt(process.version.slice(1), 10) < 16);
170-
const result = await runInlineTest({
171-
'package.json': JSON.stringify({ type: 'module' }),
172-
'playwright.config.ts': `
173-
export default {
174-
projects: [{name: 'foo'}],
175-
};
176-
`,
177-
'tsconfig.json': `{
178-
"compilerOptions": {
179-
"target": "ES2019",
180-
"module": "commonjs",
181-
"lib": ["esnext", "dom", "DOM.Iterable"],
182-
"baseUrl": ".",
183-
"paths": {
184-
"util/*": ["./foo/bar/util/*"],
185-
},
186-
},
187-
}`,
188-
'a.test.ts': `
189-
import { foo } from 'util/b.ts';
190-
const { test } = pwt;
191-
test('check project name', ({}, testInfo) => {
192-
expect(testInfo.project.name).toBe(foo);
193-
});
194-
`,
195-
'foo/bar/util/b.ts': `
196-
export const foo: string = 'foo';
197-
`,
198-
}, {}, {
199-
PW_EXPERIMENTAL_TS_ESM: true
200-
});
201-
202-
expect(result.exitCode).toBe(0);
203-
});

0 commit comments

Comments
 (0)