feat: add create-playwright package (#8841)

This commit is contained in:
Max Schmitt 2021-09-13 22:05:38 +02:00 committed by GitHub
parent 64f9c3ba1d
commit 5eca7aa1d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 4531 additions and 0 deletions

View File

@ -13,3 +13,4 @@ browser_patches/chromium/output/
**/*.d.ts
output/
/test-results/
packages/create-playwright/assets/**

View File

@ -0,0 +1,26 @@
name: create-playwright CI
on:
push:
branches: [ master ]
paths:
- 'packages/create-playwright/**/*'
pull_request:
branches: [ master ]
paths:
- 'packages/create-playwright/**/*'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/create-playwright/
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: 14
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run test

4
packages/create-playwright/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
yarn-error.log
lib/
test-results/

View File

@ -0,0 +1 @@
!lib/**/*

View File

@ -0,0 +1,12 @@
# Create Playwright
> Getting started with Playwright with a single command
[![npm](https://img.shields.io/npm/v/create-playwright)](https://www.npmjs.com/package/create-playwright)
[![create-playwright CI](https://github.com/microsoft/playwright/actions/workflows/package_create_playwright.yml/badge.svg)](https://github.com/microsoft/playwright/actions/workflows/package_create_playwright.yml)
```bash
npm init playwright
# Or for Yarn
yarn create playwright
```

View File

@ -0,0 +1,8 @@
// @ts-check
const { test, expect } = require('@playwright/test');
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.locator('text=Get Started').click();
await expect(page).toHaveTitle(/Getting Started/);
});

View File

@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.locator('text=Get Started').click();
await expect(page).toHaveTitle(/Getting Started/);
});

View File

@ -0,0 +1,21 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
build:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: {{installDepsCommand}}
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: {{runTestsCommand}}

View File

@ -0,0 +1,55 @@
// @ts-check
const { devices } = require('@playwright/test');
const path = require('path')
/**
* @see https://playwright.dev/docs/test-configuration
* @type{import('@playwright/test').PlaywrightTestConfig}
* */
const config = {
timeout: 30 * 1000,
testDir: path.join(__dirname, '{{testDir}}'),
// Run your local dev server before starting the tests:
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
projects: [
{
name: 'Desktop Chromium',
use: {
browserName: 'chromium',
// Test against Chrome Beta channel.
channel: 'chrome-beta',
},
},
{
name: 'Desktop Safari',
use: {
browserName: 'webkit',
viewport: { width: 1200, height: 750 },
}
},
// Test against mobile viewports.
{
name: 'Mobile Chrome',
use: devices['Pixel 5'],
},
{
name: 'Mobile Safari',
use: devices['iPhone 12'],
},
{
name: 'Desktop Firefox',
use: {
browserName: 'firefox',
viewport: { width: 800, height: 600 },
}
},
],
};
module.exports = config;

View File

@ -0,0 +1,50 @@
import { PlaywrightTestConfig, devices } from '@playwright/test';
import path from 'path';
// Reference: https://playwright.dev/docs/test-configuration
const config: PlaywrightTestConfig = {
timeout: 30 * 1000,
testDir: path.join(__dirname, '{{testDir}}'),
// Run your local dev server before starting the tests:
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
projects: [
{
name: 'Chrome Stable',
use: {
browserName: 'chromium',
// Test against Chrome Stable channel.
channel: 'chrome',
},
},
{
name: 'Desktop Safari',
use: {
browserName: 'webkit',
viewport: { width: 1200, height: 750 },
}
},
// Test against mobile viewports.
{
name: 'Mobile Chrome',
use: devices['Pixel 5'],
},
{
name: 'Mobile Safari',
use: devices['iPhone 12'],
},
{
name: 'Desktop Firefox',
use: {
browserName: 'firefox',
viewport: { width: 800, height: 600 },
}
},
],
};
export default config;

View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
require('./lib/index')

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
{
"name": "create-playwright",
"version": "0.1.4",
"description": "Getting started with writing end-to-end tests with Playwright.",
"repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev",
"author": "Microsoft",
"engines": {
"node": ">=12"
},
"main": "index.js",
"bin": {
"create-playwright": "./index.js"
},
"license": "MIT",
"scripts": {
"prepublish": "npm run build",
"watch": "cd src && ncc build index.ts --watch --out ../lib && cd ..",
"build": "cd src && ncc build index.ts --minify --out ../lib && cd ..",
"test": "npx playwright test"
},
"devDependencies": {
"@playwright/test": "^1.15.0-next-alpha-sep-10-2021",
"@types/node": "^16.9.0",
"@vercel/ncc": "^0.30.0",
"enquirer": "^2.3.6",
"typescript": "^4.4.2"
}
}

View File

@ -0,0 +1,24 @@
/**
* 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 { PlaywrightTestConfig } from '@playwright/test';
import path from 'path';
const config: PlaywrightTestConfig = {
timeout: 120 * 1000,
testDir: path.join(__dirname, 'tests'),
};
export default config;

View File

@ -0,0 +1,151 @@
/**
* 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 path from 'path';
import fs from 'fs';
import { prompt } from 'enquirer';
import colors from 'ansi-colors';
import { executeCommands, createFiles, determinePackageManager, executeTemplate, determineRootDir } from './utils';
export type PromptOptions = {
testDir: string,
installGitHubActions: boolean,
language: 'JavaScript' | 'TypeScript'
};
class Generator {
packageManager: 'npm' | 'yarn';
constructor(private readonly rootDir: string) {
if (!fs.existsSync(rootDir))
fs.mkdirSync(rootDir);
this.packageManager = determinePackageManager(this.rootDir);
}
async run() {
this._printIntro();
const questions = await this._askQuestions();
const { files, commands } = await this._identifyChanges(questions);
executeCommands(this.rootDir, commands);
await createFiles(this.rootDir, files);
await this._patchPackageJSON();
this._printOutro();
}
private _printIntro() {
console.log(colors.yellow(`Getting started with writing ${colors.bold('end-to-end')} tests with ${colors.bold('Playwright')}:`));
console.log(`Initializing project in '${path.relative(process.cwd(), this.rootDir) || '.'}'`);
}
private async _askQuestions() {
if (process.env.TEST_OPTIONS)
return JSON.parse(process.env.TEST_OPTIONS);
return await prompt<PromptOptions>([
{
type: 'select',
name: 'language',
message: 'Do you want to use TypeScript or JavaScript?',
choices: [
{ name: 'TypeScript' },
{ name: 'JavaScript' },
],
},
{
type: 'text',
name: 'testDir',
message: 'Where to put your integration tests?',
initial: 'e2e'
},
{
type: 'confirm',
name: 'installGitHubActions',
message: 'Add GitHub Actions workflow?',
initial: true,
},
]);
}
private async _identifyChanges(options: PromptOptions) {
const commands: string[] = [];
const files = new Map<string, string>();
const fileExtension = options.language === 'JavaScript' ? 'js' : 'ts';
files.set(`playwright.config.${fileExtension}`, executeTemplate(this._readAsset(`playwright.config.${fileExtension}`), {
testDir: options.testDir,
}));
if (options.installGitHubActions) {
const githubActionsScript = executeTemplate(this._readAsset('github-actions.yml'), {
installDepsCommand: this.packageManager === 'npm' ? 'npm ci' : 'yarn',
runTestsCommand: commandToRunTests(this.packageManager),
});
files.set('.github/workflows/playwright.yml', githubActionsScript);
}
files.set(path.join(options.testDir, `example.spec.${fileExtension}`), this._readAsset(`example.spec.${fileExtension}`));
if (!fs.existsSync(path.join(this.rootDir, 'package.json')))
commands.push(this.packageManager === 'yarn' ? 'yarn init -y' : 'npm init -y');
if (this.packageManager === 'yarn')
commands.push('yarn add --dev @playwright/test');
else
commands.push('npm install --save-dev @playwright/test');
commands.push('npx playwright install --with-deps');
return { files, commands };
}
private _readAsset(asset: string): string {
const assetsDir = path.join(__dirname, '..', 'assets');
return fs.readFileSync(path.join(assetsDir, asset), 'utf-8');
}
private async _patchPackageJSON() {
const packageJSON = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf-8'));
if (!packageJSON.scripts)
packageJSON.scripts = {};
packageJSON.scripts['playwright-tests'] = `playwright test`;
const files = new Map<string, string>();
files.set('package.json', JSON.stringify(packageJSON, null, 2));
await createFiles(this.rootDir, files, true);
}
private _printOutro() {
console.log(colors.green('✔'), colors.bold('Successfully initialized your Playwright Test project!'));
const pathToNavigate = path.relative(process.cwd(), this.rootDir);
const prefix = pathToNavigate !== '' ? `- cd ${pathToNavigate}\n` : '';
console.log(colors.bold('🎭 Try it out with:\n') + colors.greenBright(prefix + '- ' + commandToRunTests(this.packageManager)));
console.log('Visit https://playwright.dev/docs/intro for more information');
}
}
export function commandToRunTests(packageManager: 'npm' | 'yarn') {
if (packageManager === 'yarn')
return 'yarn playwright-tests';
return 'npm run playwright-tests';
}
(async () => {
const rootDir = determineRootDir();
const generator = new Generator(rootDir);
await generator.run();
})().catch(error => {
console.error(error);
process.exit(1);
});

View File

@ -0,0 +1,74 @@
/**
* 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 fs from 'fs';
import { execSync } from 'child_process';
import path from 'path';
import { prompt } from 'enquirer';
import colors from 'ansi-colors';
export function executeCommands(cwd: string, commands: string[]) {
for (const command of commands) {
console.log('Running:', command);
execSync(command, {
stdio: 'inherit',
cwd,
});
}
}
export async function createFiles(rootDir: string, files: Map<string, string>, force: boolean = false) {
for (const [relativeFilePath, value] of files) {
const absoluteFilePath = path.join(rootDir, relativeFilePath);
if (fs.existsSync(absoluteFilePath) && !force) {
const { override } = await prompt<{ override: boolean }>({
type: 'confirm',
name: 'override',
message: `${absoluteFilePath} already exists. Should it override?`,
initial: false
});
if (!override)
continue;
}
console.log(colors.gray(`Writing ${path.relative(process.cwd(), absoluteFilePath)}.`));
fs.mkdirSync(path.dirname(absoluteFilePath), { recursive: true });
fs.writeFileSync(absoluteFilePath, value, 'utf-8');
}
}
export function determineRootDir() {
const givenPath = process.argv[2];
if (givenPath)
return path.isAbsolute(givenPath) ? process.argv[2] : path.join(process.cwd(), process.argv[2]);
return process.cwd();
}
export function determinePackageManager(rootDir: string): 'yarn' | 'npm' {
if (fs.existsSync(path.join(rootDir, 'yarn.lock')))
return 'yarn';
if (process.env.npm_config_user_agent)
return process.env.npm_config_user_agent.includes('yarn') ? 'yarn' : 'npm';
return 'npm';
}
export function executeTemplate(input: string, args: Record<string, string>): string {
for (const key in args)
input = input.replace(`{{${key}}}`, args[key]);
return input;
}

View File

@ -0,0 +1,123 @@
/**
* 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, expect } from '@playwright/test';
import { spawn } from 'child_process';
import path from 'path';
import fs from 'fs';
import type { PromptOptions } from '../src';
type TestFixtures = {
packageManager: 'npm' | 'yarn';
run: (parameters: string[], options: PromptOptions) => Promise<RunResult>,
};
type RunResult = {
exitCode: number,
dir: string,
stderr: string,
stdout: string,
};
const test = base.extend<TestFixtures>({
packageManager: 'npm',
run: async ({ packageManager }, use, testInfo) => {
await use(async (parameters: string[], options: PromptOptions): Promise<RunResult> => {
fs.mkdirSync(testInfo.outputDir, { recursive: true });
const env = packageManager === 'yarn' ? {
'npm_config_user_agent': 'yarn'
} : undefined;
const p = spawn('node', [path.join(__dirname, '..'), ...parameters], {
shell: true,
cwd: testInfo.outputDir,
env: {
...process.env,
...env,
'TEST_OPTIONS': JSON.stringify(options),
}
});
let stdout = '';
let stderr = '';
p.stdout.on('data', data => stdout += data.toString());
p.stderr.on('data', data => stderr += data.toString());
let resolve = (result: RunResult) => { };
const waitUntilExit = new Promise<RunResult>(r => resolve = r);
p.on('exit', exitCode => {
resolve({ exitCode, dir: testInfo.outputDir, stdout, stderr });
});
if (process.env.DEBUG) {
p.stdout.on('data', chunk => process.stdout.write(chunk));
p.stderr.on('data', chunk => process.stderr.write(chunk));
}
return await waitUntilExit;
});
},
});
for (const packageManager of ['npm', 'yarn'] as ('npm'|'yarn')[]) {
test.describe(`Package manager: ${packageManager}`, () => {
test.use({packageManager});
test('should generate a project in the current directory', async ({ run }) => {
const { exitCode, dir, stdout } = await run([], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript' });
expect(exitCode).toBe(0);
expect(fs.existsSync(path.join(dir, 'e2e/example.spec.ts'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy();
if (packageManager === 'npm')
expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBeTruthy();
else
expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy();
const playwrightConfigContent = fs.readFileSync(path.join(dir, 'playwright.config.ts'), 'utf8');
expect(playwrightConfigContent).toContain('e2e');
expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy();
if (packageManager === 'npm') {
expect(stdout).toContain('Running: npm init -y');
expect(stdout).toContain('Running: npm install --save-dev @playwright/test');
} else {
expect(stdout).toContain('Running: yarn init -y');
expect(stdout).toContain('Running: yarn add --dev @playwright/test');
}
expect(stdout).toContain('Running: npx playwright install --with-deps');
});
test('should generate a project in a given directory', async ({ run }) => {
const { exitCode, dir } = await run(['foobar'], { installGitHubActions: true, testDir: 'e2e', language: 'TypeScript' });
expect(exitCode).toBe(0);
expect(fs.existsSync(path.join(dir, 'foobar/e2e/example.spec.ts'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'foobar/package.json'))).toBeTruthy();
if (packageManager === 'npm')
expect(fs.existsSync(path.join(dir, 'foobar/package-lock.json'))).toBeTruthy();
else
expect(fs.existsSync(path.join(dir, 'foobar/yarn.lock'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'foobar/playwright.config.ts'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'foobar/.github/workflows/playwright.yml'))).toBeTruthy();
});
test('should generate a project with JavaScript and without GHA', async ({ run }) => {
const { exitCode, dir } = await run([], { installGitHubActions: false, testDir: 'tests', language: 'JavaScript' });
expect(exitCode).toBe(0);
expect(fs.existsSync(path.join(dir, 'tests/example.spec.js'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy();
if (packageManager === 'npm')
expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBeTruthy();
else
expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, 'playwright.config.js'))).toBeTruthy();
expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy();
});
});
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"moduleResolution": "Node",
"downlevelIteration": true,
"esModuleInterop": true,
},
"files": [
"src/index.ts"
]
}