diff --git a/.eslintignore b/.eslintignore
index 006f77c58c..38c19d93ec 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -14,6 +14,7 @@ browser_patches/chromium/output/
output/
test-results/
tests/components/
+tests/installation/fixture-scripts/
examples/
DEPS
.cache/
\ No newline at end of file
diff --git a/packages/playwright-core/src/utils/spawnAsync.ts b/packages/playwright-core/src/utils/spawnAsync.ts
index 9eb33df7b9..8e286fad47 100644
--- a/packages/playwright-core/src/utils/spawnAsync.ts
+++ b/packages/playwright-core/src/utils/spawnAsync.ts
@@ -24,9 +24,9 @@ export function spawnAsync(cmd: string, args: string[], options: SpawnOptions =
let stdout = '';
let stderr = '';
if (process.stdout)
- process.stdout.on('data', data => stdout += data);
+ process.stdout.on('data', data => stdout += data.toString());
if (process.stderr)
- process.stderr.on('data', data => stderr += data);
+ process.stderr.on('data', data => stderr += data.toString());
process.on('close', code => resolve({ stdout, stderr, code }));
process.on('error', error => resolve({ stdout, stderr, code: 0, error }));
});
diff --git a/tests/installation/fixture-scripts/playwright-test-plugin-types.ts b/tests/installation/fixture-scripts/playwright-test-plugin-types.ts
new file mode 100644
index 0000000000..c18826a078
--- /dev/null
+++ b/tests/installation/fixture-scripts/playwright-test-plugin-types.ts
@@ -0,0 +1,13 @@
+import { test as test1 } from '@playwright/test';
+import type { Page } from '@playwright/test';
+import { test as test2 } from 'playwright-test-plugin';
+
+const test = (test1 as any)._extendTest(test2);
+
+test('sample test', async ({ page, plugin }) => {
+ type IsPage = (typeof page) extends Page ? true : never;
+ const isPage: IsPage = true;
+
+ type IsString = (typeof plugin) extends string ? true : never;
+ const isString: IsString = true;
+});
diff --git a/tests/installation/fixture-scripts/plugin.spec.ts b/tests/installation/fixture-scripts/plugin.spec.ts
new file mode 100644
index 0000000000..fdf1f42b27
--- /dev/null
+++ b/tests/installation/fixture-scripts/plugin.spec.ts
@@ -0,0 +1,12 @@
+import { test as test1, expect } from '@playwright/test';
+import { test as test2 } from 'playwright-test-plugin';
+
+const test = (test1 as any)._extendTest(test2);
+
+test('sample test', async ({ page, plugin }) => {
+ await page.setContent(`
hello
world`);
+ expect(await page.textContent('span')).toBe('world');
+
+ console.log(`plugin value: ${plugin}`);
+ expect(plugin).toBe('hello from plugin');
+});
diff --git a/tests/installation/globalSetup.ts b/tests/installation/globalSetup.ts
index 88b95e60ab..8f9bf4b199 100644
--- a/tests/installation/globalSetup.ts
+++ b/tests/installation/globalSetup.ts
@@ -65,6 +65,22 @@ async function globalSetup() {
build('playwright-browser-webkit', '@playwright/browser-webkit'),
]);
+ const buildPlaywrightTestPlugin = async () => {
+ const cwd = path.resolve(path.join(__dirname, `playwright-test-plugin`));
+ const tscResult = await spawnAsync('npx', ['tsc', '-p', 'tsconfig.json'], { cwd, shell: process.platform === 'win32' });
+ if (tscResult.code)
+ throw new Error(`Failed to build playwright-test-plugin:\n${tscResult.stderr}\n${tscResult.stdout}`);
+ const packResult = await spawnAsync('npm', ['pack'], { cwd, shell: process.platform === 'win32' });
+ if (packResult.code)
+ throw new Error(`Failed to build playwright-test-plugin:\n${packResult.stderr}\n${packResult.stdout}`);
+ const tgzName = packResult.stdout.trim();
+ const outPath = path.resolve(path.join(outputDir, `playwright-test-plugin.tgz`));
+ await fs.promises.rename(path.join(cwd, tgzName), outPath);
+ console.log('Built playwright-test-plugin');
+ return ['playwright-test-plugin', outPath];
+ };
+ builds.push(await buildPlaywrightTestPlugin());
+
await fs.promises.writeFile(path.join(__dirname, '.registry.json'), JSON.stringify(Object.fromEntries(builds)));
}
diff --git a/tests/installation/npmTest.ts b/tests/installation/npmTest.ts
index 987f75477e..0f2f9e7120 100644
--- a/tests/installation/npmTest.ts
+++ b/tests/installation/npmTest.ts
@@ -20,10 +20,11 @@ import os from 'os';
import path from 'path';
import debugLogger from 'debug';
import { Registry } from './registry';
-import { spawnAsync } from './spawnAsync';
import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
import { commonFixtures } from '../config/commonFixtures';
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
+import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
+import type { SpawnOptions } from 'child_process';
export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os.tmpdir(), 'pwt', 'workspaces');
@@ -56,7 +57,7 @@ const expect = _expect.extend({
}
});
-type ExecOptions = { cwd?: string, env?: Record, message?: string, expectToExitWithError?: boolean };
+type ExecOptions = SpawnOptions & { message?: string, expectToExitWithError?: boolean };
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
type NPMTestOptions = {
@@ -71,7 +72,7 @@ type NPMTestFixtures = {
writeConfig: (allowGlobal: boolean) => Promise;
writeFiles: (nameToContents: Record) => Promise;
exec: (cmd: string, ...argsAndOrOptions: ArgsOrOptions) => Promise;
- tsc: (...argsAndOrOptions: ArgsOrOptions) => Promise;
+ tsc: (args: string) => Promise;
registry: Registry;
};
@@ -153,7 +154,7 @@ export const test = _test
args = argsAndOrOptions as string[];
- let result!: Awaited>;
+ let result!: {stdout: string, stderr: string, code: number | null, error?: Error};
await test.step(`exec: ${[cmd, ...args].join(' ')}`, async () => {
result = await spawnAsync(cmd, args, {
shell: true,
@@ -197,8 +198,8 @@ export const test = _test
});
},
tsc: async ({ exec }, use) => {
- await exec('npm i --foreground-scripts typescript@3.8 @types/node@14');
- await use((...args: ArgsOrOptions) => exec('npx', '-p', 'typescript@4.1.6', 'tsc', ...args));
+ await exec('npm i --foreground-scripts typescript@5.2.2 @types/node@16');
+ await use((args: string) => exec('npx', 'tsc', args, { shell: process.platform === 'win32' }));
},
});
diff --git a/tests/installation/playwright-test-plugin.spec.ts b/tests/installation/playwright-test-plugin.spec.ts
new file mode 100755
index 0000000000..deadc94da5
--- /dev/null
+++ b/tests/installation/playwright-test-plugin.spec.ts
@@ -0,0 +1,75 @@
+/**
+ * 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 './npmTest';
+import path from 'path';
+import fs from 'fs';
+
+function patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace: string) {
+ // It is not currently possible to declare plugin's peerDependency to match
+ // various pre-release versions, e.g. "1.38.0-next" and "1.39.1-alpha".
+ // See https://github.com/npm/rfcs/pull/397 and https://github.com/npm/node-semver#prerelease-tags.
+ //
+ // Workaround per https://stackoverflow.com/questions/71479750/npm-install-pre-release-versions-for-peer-dependency.
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(tmpWorkspace, 'package.json'), 'utf-8'));
+ if (pkg.dependencies['@playwright/test'].match(/\d+\.\d+-\w+/)) {
+ console.log(`Setting overrides in package.json to make pre-release version of peer dependency work.`);
+ pkg.overrides = { '@playwright/test': '$@playwright/test' };
+ fs.writeFileSync(path.resolve(tmpWorkspace, 'package.json'), JSON.stringify(pkg, null, 2));
+ }
+}
+
+test('npm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
+ await exec('npm i --foreground-scripts @playwright/test');
+ patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
+ await exec('npm i --foreground-scripts playwright-test-plugin');
+ await exec('npx playwright install chromium');
+
+ const output = await exec('npx playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
+ expect(output).toContain('plugin value: hello from plugin');
+ expect(output).toContain('1 passed');
+
+ await exec('npm i --foreground-scripts typescript@5.2.2 @types/node@16');
+ await exec('npx tsc playwright-test-plugin-types.ts');
+});
+
+test('pnpm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
+ await exec('pnpm add @playwright/test');
+ patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
+ await exec('pnpm add playwright-test-plugin');
+ await exec('pnpm exec playwright install chromium');
+
+ const output = await exec('pnpm exec playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
+ expect(output).toContain('plugin value: hello from plugin');
+ expect(output).toContain('1 passed');
+
+ await exec('pnpm add typescript@5.2.2 @types/node@16');
+ await exec('pnpm exec tsc playwright-test-plugin-types.ts');
+});
+
+test('yarn: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
+ await exec('yarn add @playwright/test');
+ patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
+ await exec('yarn add playwright-test-plugin');
+ await exec('yarn playwright install chromium');
+
+ const output = await exec('yarn playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
+ expect(output).toContain('plugin value: hello from plugin');
+ expect(output).toContain('1 passed');
+
+ await exec('yarn add typescript@5.2.2 @types/node@16');
+ await exec('yarn tsc playwright-test-plugin-types.ts');
+});
diff --git a/tests/installation/playwright-test-plugin/.gitignore b/tests/installation/playwright-test-plugin/.gitignore
new file mode 100644
index 0000000000..a1b3dc8d51
--- /dev/null
+++ b/tests/installation/playwright-test-plugin/.gitignore
@@ -0,0 +1,2 @@
+**/*.js
+**/*.d.ts
diff --git a/tests/installation/playwright-test-plugin/.npmignore b/tests/installation/playwright-test-plugin/.npmignore
new file mode 100644
index 0000000000..0f85d6c6a8
--- /dev/null
+++ b/tests/installation/playwright-test-plugin/.npmignore
@@ -0,0 +1,2 @@
+**/*.ts
+!**/*.d.ts
diff --git a/tests/installation/playwright-test-plugin/index.ts b/tests/installation/playwright-test-plugin/index.ts
new file mode 100644
index 0000000000..8448dce151
--- /dev/null
+++ b/tests/installation/playwright-test-plugin/index.ts
@@ -0,0 +1,23 @@
+/**
+ * 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 as base } from '@playwright/test';
+
+export const test = base.extend<{ plugin: string }>({
+ plugin: async ({}, use) => {
+ await use('hello from plugin');
+ },
+});
diff --git a/tests/installation/playwright-test-plugin/package.json b/tests/installation/playwright-test-plugin/package.json
new file mode 100644
index 0000000000..4fac36726e
--- /dev/null
+++ b/tests/installation/playwright-test-plugin/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "playwright-test-plugin",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "peerDependencies": {
+ "@playwright/test": "1.x"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/tests/installation/playwright-test-plugin/tsconfig.json b/tests/installation/playwright-test-plugin/tsconfig.json
new file mode 100644
index 0000000000..d91382771e
--- /dev/null
+++ b/tests/installation/playwright-test-plugin/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2019",
+ "module": "commonjs",
+ "lib": ["esnext", "dom", "DOM.Iterable"],
+ "esModuleInterop": true,
+ "strict": true,
+ "allowJs": false,
+ "resolveJsonModule": true,
+ "noImplicitOverride": true,
+ "useUnknownInCatchVariables": false,
+ "declaration": true,
+ },
+}
diff --git a/tests/installation/playwright-test-should-work.spec.ts b/tests/installation/playwright-test-should-work.spec.ts
index 4d52341a49..ab6c3b9411 100755
--- a/tests/installation/playwright-test-should-work.spec.ts
+++ b/tests/installation/playwright-test-should-work.spec.ts
@@ -18,7 +18,7 @@ import path from 'path';
test('npm: @playwright/test should work', async ({ exec, tmpWorkspace }) => {
await exec('npm i --foreground-scripts @playwright/test');
- await exec('npx playwright test -c .', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
+ await exec('npx playwright test -c . sample.spec.js', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
await exec('npx playwright install');
await exec('npx playwright test -c . --browser=all --reporter=list,json sample.spec.js', { env: { PLAYWRIGHT_JSON_OUTPUT_NAME: 'report.json' } });
diff --git a/tests/installation/registry.ts b/tests/installation/registry.ts
index ee7e68d00c..d267f9be56 100644
--- a/tests/installation/registry.ts
+++ b/tests/installation/registry.ts
@@ -19,7 +19,7 @@ import type { Server } from 'http';
import type http from 'http';
import https from 'https';
import path from 'path';
-import { spawnAsync } from './spawnAsync';
+import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
const kPublicNpmRegistry = 'https://registry.npmjs.org';
diff --git a/tests/installation/spawnAsync.ts b/tests/installation/spawnAsync.ts
deleted file mode 100644
index f1009c0f28..0000000000
--- a/tests/installation/spawnAsync.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * 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 type { SpawnOptions } from 'child_process';
-import { spawn } from 'child_process';
-import debugLogger from 'debug';
-
-const debugExec = debugLogger('itest:exec');
-const debugExecStdout = debugLogger('itest:exec:stdout');
-const debugExecStderr = debugLogger('itest:exec:stderr');
-
-export function spawnAsync(cmd: string, args: string[], options: SpawnOptions = {}): Promise<{stdout: string, stderr: string, code: number | null, error?: Error}> {
- // debugExec(`CWD: ${options.cwd || process.cwd()}`);
- // debugExec(`ENV: ${Object.entries(options.env || {}).map(([key, value]) => `${key}=${value}`).join(' ')}`);
- debugExec([cmd, ...args].join(' '));
- const p = spawn(cmd, args, Object.assign({ windowsHide: true }, options));
-
- return new Promise(resolve => {
- let stdout = '';
- let stderr = '';
- if (p.stdout) {
- p.stdout.on('data', data => {
- debugExecStdout(data.toString());
- stdout += data;
- });
- }
- if (p.stderr) {
- p.stderr.on('data', data => {
- debugExecStderr(data.toString());
- stderr += data;
- });
- }
- p.on('close', code => resolve({ stdout, stderr, code }));
- p.on('error', error => resolve({ stdout, stderr, code: 0, error }));
- });
-}
diff --git a/tests/installation/typescript-types.spec.ts b/tests/installation/typescript-types.spec.ts
index 26748d722e..139eec9925 100755
--- a/tests/installation/typescript-types.spec.ts
+++ b/tests/installation/typescript-types.spec.ts
@@ -37,8 +37,6 @@ test('typescript types should work', async ({ exec, tsc, writeFiles }) => {
});
test('typescript types should work with module: NodeNext', async ({ exec, tsc, writeFiles }) => {
- // module: NodeNext got added in TypeScript 4.7
- await exec('npm i --foreground-scripts typescript@4.7 @types/node@18');
const libraryPackages = [
'playwright',
'playwright-core',
@@ -53,8 +51,8 @@ test('typescript types should work with module: NodeNext', async ({ exec, tsc, w
await writeFiles({
[filename]: `import { Page } from '${libraryPackage}';`,
});
- await exec('npx', '-p', 'typescript@4.7', 'tsc', '--module nodenext', filename);
+ await tsc(`--module nodenext ${filename}`);
}
- await exec('npx', '-p', 'typescript@4.7', 'tsc', '--module nodenext', 'playwright-test-types.ts');
+ await tsc('--module nodenext playwright-test-types.ts');
});
diff --git a/tests/tsconfig.json b/tests/tsconfig.json
index 361dcb03b4..adf10e87bd 100644
--- a/tests/tsconfig.json
+++ b/tests/tsconfig.json
@@ -19,5 +19,5 @@
},
},
"include": ["**/*.spec.js", "**/*.ts", "index.d.ts"],
- "exclude": ["components/"]
+ "exclude": ["components/", "installation/fixture-scripts/"]
}