├── .gitignore ├── index.js ├── src ├── tsconfig.json ├── cli.ts ├── utils.ts ├── packageManager.ts └── generator.ts ├── assets ├── playwright │ ├── index.js │ └── index.html ├── tsconfig.json ├── .eslintrc.js ├── example.spec.ts ├── example.spec.js ├── github-actions.yml ├── playwright-ct.config.ts ├── playwright-ct.config.js ├── playwright.config.ts └── playwright.config.js ├── .github ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── publish.yml │ └── ci.yml ├── .npmignore ├── CODE_OF_CONDUCT.md ├── tsconfig.json ├── README.md ├── package.json ├── LICENSE ├── utils └── update_canary_version.js ├── SUPPORT.md ├── playwright.config.ts ├── tests ├── component-testing.spec.ts ├── baseFixtures.ts └── integration.spec.ts └── SECURITY.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | test-results/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./lib/index') 3 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /assets/playwright/index.js: -------------------------------------------------------------------------------- 1 | // Import styles, initialize component theme here. 2 | // import '../src/common.css'; 3 | -------------------------------------------------------------------------------- /assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noEmit": true, 5 | "jsx": "react-jsx", 6 | }, 7 | "include": ["./src"], 8 | } 9 | -------------------------------------------------------------------------------- /assets/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Example files here do not need the copyright notice. 2 | module.exports = { 3 | 'extends': '../../../.eslintrc.js', 4 | 'rules': { 5 | 'notice/notice': 0 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: File an issue 4 | url: https://github.com/microsoft/playwright/issues 5 | about: We track issues in the microsoft/playwright repository 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # This ignores everything by default, except for package.json and LICENSE and README.md. 2 | # See https://docs.npmjs.com/misc/developers 3 | **/* 4 | 5 | !/lib/**/* 6 | !index.js 7 | 8 | !/assets/**/* 9 | /assets/.eslintrc.js 10 | -------------------------------------------------------------------------------- /assets/playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Testing Page 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "allowJs": true, 8 | "jsx": "react", 9 | "resolveJsonModule": true, 10 | "noEmit": true, 11 | "noImplicitOverride": true, 12 | "useUnknownInCatchVariables": false, 13 | "noUncheckedIndexedAccess": true 14 | }, 15 | "compileOnSave": true, 16 | "include": ["src", "tests", "assets"], 17 | "exclude": ["assets/playwright-ct.config.ts", "assets/playwright-ct.config.js"] 18 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create Playwright 2 | 3 | > Getting started with [Playwright](https://github.com/microsoft/playwright) with a single command 4 | 5 | [![npm](https://img.shields.io/npm/v/create-playwright)](https://www.npmjs.com/package/create-playwright) 6 | [![create-playwright CI](https://github.com/microsoft/create-playwright/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/create-playwright/actions/workflows/ci.yml) 7 | 8 | ```bash 9 | npm init playwright@latest 10 | # Or for Yarn 11 | yarn create playwright 12 | # Or for pnpm 13 | pnpm create playwright 14 | ``` 15 | -------------------------------------------------------------------------------- /assets/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | }); 9 | 10 | test('get started link', async ({ page }) => { 11 | await page.goto('https://playwright.dev/'); 12 | 13 | // Click the get started link. 14 | await page.getByRole('link', { name: 'Get started' }).click(); 15 | 16 | // Expects page to have a heading with the name of Installation. 17 | await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); 18 | }); 19 | -------------------------------------------------------------------------------- /assets/example.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from '@playwright/test'; 3 | 4 | test('has title', async ({ page }) => { 5 | await page.goto('https://playwright.dev/'); 6 | 7 | // Expect a title "to contain" a substring. 8 | await expect(page).toHaveTitle(/Playwright/); 9 | }); 10 | 11 | test('get started link', async ({ page }) => { 12 | await page.goto('https://playwright.dev/'); 13 | 14 | // Click the get started link. 15 | await page.getByRole('link', { name: 'Get started' }).click(); 16 | 17 | // Expects page to have a heading with the name of Installation. 18 | await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /assets/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | - name: Install dependencies 17 | run: {{installDepsCommand}} 18 | - name: Install Playwright Browsers 19 | run: {{installPlaywrightCommand}} 20 | - name: Run Playwright tests 21 | run: {{runTestsCommand}} 22 | - uses: actions/upload-artifact@v4 23 | if: ${{ !cancelled() }} 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM 2 | on: 3 | workflow_dispatch: 4 | release: 5 | types: [published] 6 | jobs: 7 | publish-npm: 8 | runs-on: ubuntu-22.04 9 | permissions: 10 | contents: read 11 | id-token: write # Required for OIDC 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | registry-url: https://registry.npmjs.org/ 18 | # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing) 19 | - name: Update npm 20 | run: npm install -g npm@latest 21 | - run: npm ci 22 | - name: Publish release to NPM 23 | if: github.event_name == 'release' 24 | run: npm publish 25 | - name: Publish @next to NPM 26 | if: github.event_name == 'workflow_dispatch' 27 | run: | 28 | node utils/update_canary_version.js 29 | npm publish --tag=next 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: create-playwright CI 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | node-version: [18.x, 20.x, 22.x, 24.x] 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | cache: 'npm' 22 | - run: npm i -g yarn@1 23 | - run: npm i -g pnpm@10 24 | - run: npm ci 25 | - run: npx playwright install-deps 26 | - run: npm run build 27 | - run: npx tsc --noEmit 28 | - run: npx playwright test --grep-invert yarn-berry 29 | - run: corepack enable 30 | - run: corepack prepare yarn@4 --activate 31 | - run: npx playwright test --grep yarn-berry 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-playwright", 3 | "version": "1.17.139", 4 | "description": "Getting started with writing end-to-end tests with Playwright.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/microsoft/create-playwright.git" 8 | }, 9 | "homepage": "https://playwright.dev", 10 | "author": { 11 | "name": "Microsoft Corporation" 12 | }, 13 | "engines": { 14 | "node": ">=18" 15 | }, 16 | "main": "index.js", 17 | "bin": { 18 | "create-playwright": "./index.js" 19 | }, 20 | "license": "Apache-2.0", 21 | "scripts": { 22 | "build": "esbuild --bundle src/cli.ts --outfile=lib/index.js --platform=node --target=ES2019", 23 | "clean": "rm -rf lib", 24 | "watch": "npm run build -- --watch", 25 | "test": "playwright test", 26 | "prepublish": "npm run build" 27 | }, 28 | "devDependencies": { 29 | "@playwright/test": "^1.49.1", 30 | "@types/ini": "^4.1.1", 31 | "@types/node": "^18.19.33", 32 | "ansi-colors": "^4.1.1", 33 | "commander": "^14.0.1", 34 | "enquirer": "^2.3.6", 35 | "esbuild": "^0.25.0", 36 | "ini": "^4.1.3", 37 | "typescript": "^5.4.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /utils/update_canary_version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) Microsoft Corporation. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | const { execSync } = require('child_process'); 21 | 22 | const timestamp = execSync('git show -s --format=%ct HEAD', { 23 | stdio: ['ignore', 'pipe', 'ignore'] 24 | }).toString('utf8').trim(); 25 | 26 | const packageJSON = require('../package.json'); 27 | const newVersion = `${packageJSON.version}-alpha-${timestamp}000`; 28 | console.log('Setting version to ' + newVersion); 29 | 30 | packageJSON.version = newVersion; 31 | fs.writeFileSync(path.join(__dirname, '../package.json'), JSON.stringify(packageJSON, null, 2) + '\n'); 32 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /assets/playwright-ct.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '{{ctPackageName}}'; 2 | 3 | /** 4 | * See https://playwright.dev/docs/test-configuration. 5 | */ 6 | export default defineConfig({ 7 | testDir: './{{testDir}}', 8 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 9 | snapshotDir: './__snapshots__', 10 | /* Maximum time one test can run for. */ 11 | timeout: 10 * 1000, 12 | /* Run tests in files in parallel */ 13 | fullyParallel: true, 14 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 15 | forbidOnly: !!process.env.CI, 16 | /* Retry on CI only */ 17 | retries: process.env.CI ? 2 : 0, 18 | /* Opt out of parallel tests on CI. */ 19 | workers: process.env.CI ? 1 : undefined, 20 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 21 | reporter: 'html', 22 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 23 | use: { 24 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 25 | trace: 'on-first-retry', 26 | 27 | /* Port to use for Playwright component endpoint. */ 28 | ctPort: 3100, 29 | }, 30 | 31 | /* Configure projects for major browsers */ 32 | projects: [ 33 | { 34 | name: 'chromium', 35 | use: { ...devices['Desktop Chrome'] }, 36 | }, 37 | { 38 | name: 'firefox', 39 | use: { ...devices['Desktop Firefox'] }, 40 | }, 41 | { 42 | name: 'webkit', 43 | use: { ...devices['Desktop Safari'] }, 44 | }, 45 | ], 46 | }); 47 | -------------------------------------------------------------------------------- /assets/playwright-ct.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '{{ctPackageName}}'; 3 | 4 | /** 5 | * @see https://playwright.dev/docs/test-configuration 6 | */ 7 | module.exports = defineConfig({ 8 | testDir: './{{testDir}}', 9 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 10 | snapshotDir: './__snapshots__', 11 | /* Maximum time one test can run for. */ 12 | timeout: 10 * 1000, 13 | /* Run tests in files in parallel */ 14 | fullyParallel: true, 15 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 16 | forbidOnly: !!process.env.CI, 17 | /* Retry on CI only */ 18 | retries: process.env.CI ? 2 : 0, 19 | /* Opt out of parallel tests on CI. */ 20 | workers: process.env.CI ? 1 : undefined, 21 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 22 | reporter: 'html', 23 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 24 | use: { 25 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 26 | trace: 'on-first-retry', 27 | 28 | /* Port to use for Playwright component endpoint. */ 29 | ctPort: 3100, 30 | }, 31 | 32 | /* Configure projects for major browsers */ 33 | projects: [ 34 | { 35 | name: 'chromium', 36 | use: { ...devices['Desktop Chrome'] }, 37 | }, 38 | { 39 | name: 'firefox', 40 | use: { ...devices['Desktop Firefox'] }, 41 | }, 42 | { 43 | name: 'webkit', 44 | use: { ...devices['Desktop Safari'] }, 45 | }, 46 | ], 47 | }); 48 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 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 | import { defineConfig } from '@playwright/test'; 17 | import type { TestFixtures } from './tests/baseFixtures'; 18 | import fs from 'node:fs'; 19 | import path from 'node:path'; 20 | import os from 'node:os'; 21 | 22 | export default defineConfig({ 23 | timeout: 120 * 1000, 24 | testDir: './tests', 25 | reporter: 'list', 26 | workers: process.env.CI ? 1 : undefined, 27 | outputDir: fs.mkdtempSync(path.join(os.tmpdir(), 'create-playwright-test-')), // place test dir outside to prevent influece from `yarn.lock` or `package.json` in repo 28 | projects: [ 29 | { 30 | name: 'npm', 31 | use: { 32 | packageManager: 'npm' 33 | }, 34 | }, 35 | { 36 | name: 'yarn-classic', 37 | use: { 38 | packageManager: 'yarn-classic' 39 | } 40 | }, 41 | { 42 | name: 'yarn-berry', 43 | use: { 44 | packageManager: 'yarn-berry' 45 | } 46 | }, 47 | { 48 | name: 'pnpm', 49 | use: { 50 | packageManager: 'pnpm' 51 | } 52 | }, 53 | { 54 | name: 'pnpm-pnp', 55 | use: { 56 | packageManager: 'pnpm-pnp' 57 | } 58 | }, 59 | ] 60 | }); 61 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 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 path from 'path'; 18 | import { Command } from 'commander'; 19 | import { CliOptions, Generator } from './generator'; 20 | 21 | const program = new Command(); 22 | 23 | program 24 | .name('create-playwright') 25 | .description('Getting started with writing end-to-end tests with Playwright.') 26 | .argument('[rootDir]', 'Target directory for the Playwright project', '.') 27 | .option('--browser ', `browsers to use in default config (default: 'chromium,firefox,webkit')`) 28 | .option('--no-browsers', 'do not download browsers (can be done manually via \'npx playwright install\')') 29 | .option('--no-examples', 'do not create example test files') 30 | .option('--install-deps', 'install dependencies') 31 | .option('--next', 'install @next version of Playwright') 32 | .option('--beta', 'install @beta version of Playwright') 33 | .option('--ct', 'install Playwright Component testing') 34 | .option('--quiet', 'do not ask for interactive input prompts') 35 | .option('--gha', 'install GitHub Actions') 36 | .option('--lang ', 'language to use (js, TypeScript)') 37 | .action(async (rootDir, options) => { 38 | 39 | const cliOptions: CliOptions = { 40 | browser: options.browser, 41 | noBrowsers: !options.browsers, 42 | noExamples: !options.examples, 43 | installDeps: options.installDeps, 44 | next: options.next, 45 | beta: options.beta, 46 | ct: options.ct, 47 | quiet: options.quiet, 48 | gha: options.gha, 49 | lang: options.lang, 50 | }; 51 | const resolvedRootDir = path.resolve(process.cwd(), rootDir || '.'); 52 | const generator = new Generator(resolvedRootDir, cliOptions); 53 | await generator.run(); 54 | }); 55 | 56 | program.parseAsync().catch(error => { 57 | console.error(error); 58 | process.exit(1); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/component-testing.spec.ts: -------------------------------------------------------------------------------- 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 | import { test, expect, assertLockFilesExist, packageManagerToNpxCommand } from './baseFixtures'; 17 | import path from 'path'; 18 | import fs from 'fs'; 19 | 20 | test('should be able to generate and run a CT React project', async ({ run, dir, exec, packageManager }) => { 21 | test.skip(packageManager === 'yarn-classic' || packageManager === 'yarn-berry'); 22 | test.slow(); 23 | await run(['--ct'], { installGitHubActions: true, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true, framework: 'react' }); 24 | { 25 | expect(fs.existsSync(path.join(dir, 'playwright/index.html'))).toBeTruthy(); 26 | expect(fs.existsSync(path.join(dir, 'playwright-ct.config.ts'))).toBeTruthy(); 27 | assertLockFilesExist(dir, packageManager); 28 | } 29 | 30 | { 31 | expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); 32 | expect(fs.readFileSync(path.join(dir, '.github/workflows/playwright.yml'), 'utf8')).toContain('test-ct'); 33 | } 34 | 35 | await exec(packageManager, [((packageManager === 'yarn-classic' || packageManager === 'yarn-berry') ? 'add' : 'install'), 'react', 'react-dom']); 36 | 37 | fs.mkdirSync(path.join(dir, 'src')); 38 | fs.writeFileSync(path.join(dir, 'src/App.tsx'), 'export default () => <>Learn React;'); 39 | fs.mkdirSync(path.join(dir, 'tests')); 40 | fs.writeFileSync(path.join(dir, 'tests/App.spec.tsx'), ` 41 | import { test, expect } from '@playwright/experimental-ct-react'; 42 | import App from '../src/App'; 43 | 44 | test.use({ viewport: { width: 500, height: 500 } }); 45 | 46 | test('should work', async ({ mount }) => { 47 | const component = await mount(); 48 | await expect(component).toContainText('Learn React'); 49 | }); 50 | `); 51 | 52 | await exec(packageManagerToNpxCommand(packageManager), ['playwright', 'install-deps']); 53 | await exec(packageManager, ['run', 'test-ct']); 54 | }); 55 | -------------------------------------------------------------------------------- /assets/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './{{testDir}}', 16 | /* Run tests in files in parallel */ 17 | fullyParallel: true, 18 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 19 | forbidOnly: !!process.env.CI, 20 | /* Retry on CI only */ 21 | retries: process.env.CI ? 2 : 0, 22 | /* Opt out of parallel tests on CI. */ 23 | workers: process.env.CI ? 1 : undefined, 24 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 25 | reporter: 'html', 26 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 27 | use: { 28 | /* Base URL to use in actions like `await page.goto('')`. */ 29 | // baseURL: 'http://localhost:3000', 30 | 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: 'on-first-retry', 33 | }, 34 | 35 | /* Configure projects for major browsers */ 36 | projects: [ 37 | //--begin-chromium 38 | { 39 | name: 'chromium', 40 | use: { ...devices['Desktop Chrome'] }, 41 | }, 42 | //--end-chromium 43 | 44 | //--begin-firefox 45 | { 46 | name: 'firefox', 47 | use: { ...devices['Desktop Firefox'] }, 48 | }, 49 | //--end-firefox 50 | 51 | //--begin-webkit 52 | { 53 | name: 'webkit', 54 | use: { ...devices['Desktop Safari'] }, 55 | }, 56 | //--end-webkit 57 | 58 | /* Test against mobile viewports. */ 59 | // { 60 | // name: 'Mobile Chrome', 61 | // use: { ...devices['Pixel 5'] }, 62 | // }, 63 | // { 64 | // name: 'Mobile Safari', 65 | // use: { ...devices['iPhone 12'] }, 66 | // }, 67 | 68 | /* Test against branded browsers. */ 69 | // { 70 | // name: 'Microsoft Edge', 71 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 72 | // }, 73 | // { 74 | // name: 'Google Chrome', 75 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 76 | // }, 77 | ], 78 | 79 | /* Run your local dev server before starting the tests */ 80 | // webServer: { 81 | // command: 'npm run start', 82 | // url: 'http://localhost:3000', 83 | // reuseExistingServer: !process.env.CI, 84 | // }, 85 | }); 86 | -------------------------------------------------------------------------------- /assets/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // import dotenv from 'dotenv'; 9 | // import path from 'path'; 10 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 11 | 12 | /** 13 | * @see https://playwright.dev/docs/test-configuration 14 | */ 15 | export default defineConfig({ 16 | testDir: './{{testDir}}', 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: 'html', 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Base URL to use in actions like `await page.goto('')`. */ 30 | // baseURL: 'http://localhost:3000', 31 | 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: 'on-first-retry', 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | //--begin-chromium 39 | { 40 | name: 'chromium', 41 | use: { ...devices['Desktop Chrome'] }, 42 | }, 43 | //--end-chromium 44 | 45 | //--begin-firefox 46 | { 47 | name: 'firefox', 48 | use: { ...devices['Desktop Firefox'] }, 49 | }, 50 | //--end-firefox 51 | 52 | //--begin-webkit 53 | { 54 | name: 'webkit', 55 | use: { ...devices['Desktop Safari'] }, 56 | }, 57 | //--end-webkit 58 | 59 | /* Test against mobile viewports. */ 60 | // { 61 | // name: 'Mobile Chrome', 62 | // use: { ...devices['Pixel 5'] }, 63 | // }, 64 | // { 65 | // name: 'Mobile Safari', 66 | // use: { ...devices['iPhone 12'] }, 67 | // }, 68 | 69 | /* Test against branded browsers. */ 70 | // { 71 | // name: 'Microsoft Edge', 72 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 73 | // }, 74 | // { 75 | // name: 'Google Chrome', 76 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 77 | // }, 78 | ], 79 | 80 | /* Run your local dev server before starting the tests */ 81 | // webServer: { 82 | // command: 'npm run start', 83 | // url: 'http://localhost:3000', 84 | // reuseExistingServer: !process.env.CI, 85 | // }, 86 | }); 87 | 88 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 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 fs from 'fs'; 18 | import { execSync } from 'child_process'; 19 | import path from 'path'; 20 | 21 | import { prompt } from 'enquirer'; 22 | import colors from 'ansi-colors'; 23 | import { PromptOptions } from './generator'; 24 | 25 | export type Command = { 26 | command: string; 27 | name: string; 28 | }; 29 | 30 | export function executeCommands(cwd: string, commands: Command[]) { 31 | for (const { command, name } of commands) { 32 | console.log(`${name} (${command})…`); 33 | execSync(command, { 34 | stdio: 'inherit', 35 | cwd, 36 | }); 37 | } 38 | } 39 | 40 | export async function createFiles(rootDir: string, files: Map, force: boolean = false) { 41 | for (const [relativeFilePath, value] of files) { 42 | const absoluteFilePath = path.join(rootDir, relativeFilePath); 43 | if (fs.existsSync(absoluteFilePath) && !force) { 44 | const { override } = await prompt<{ override: boolean }>({ 45 | type: 'confirm', 46 | name: 'override', 47 | message: `${absoluteFilePath} already exists. Override it?`, 48 | initial: false 49 | }); 50 | if (!override) 51 | continue; 52 | } 53 | console.log(colors.gray(`Writing ${path.relative(process.cwd(), absoluteFilePath)}.`)); 54 | fs.mkdirSync(path.dirname(absoluteFilePath), { recursive: true }); 55 | fs.writeFileSync(absoluteFilePath, value, 'utf-8'); 56 | } 57 | } 58 | 59 | export function executeTemplate(input: string, args: Record, sections: Map): string { 60 | for (const key in args) 61 | input = input.replace(new RegExp('{{' + key + '}}', 'g'), args[key]!); 62 | const result: string[] = []; 63 | let mode: 'show' | 'hide' | 'comment' = 'show'; 64 | let indent = ''; 65 | for (const line of input.split('\n')) { 66 | const match = line.match(/(\s*)\/\/--begin-(.*)/); 67 | if (match) { 68 | mode = sections.get(match[2]!) || 'comment'; 69 | indent = match[1]!; 70 | continue; 71 | } 72 | if (line.trim().startsWith('//--end-')) { 73 | mode = 'show'; 74 | continue; 75 | } 76 | if (mode === 'show') 77 | result.push(line); 78 | else if (mode === 'comment') 79 | result.push(line.slice(0, indent.length) + '// ' + line.slice(indent.length)); 80 | } 81 | return result.join('\n'); 82 | } 83 | 84 | export function getFileExtensionCT(language: PromptOptions['language'], framework: PromptOptions['framework']) { 85 | const isJsxFramework = framework === 'solid' || framework === 'react' || framework === 'react17'; 86 | if (isJsxFramework && language === 'JavaScript') 87 | return 'jsx'; 88 | else if (isJsxFramework && language === 'TypeScript') 89 | return 'tsx'; 90 | else if (language === 'TypeScript') 91 | return 'ts'; 92 | return 'js'; 93 | } 94 | 95 | export function languageToFileExtension(language: PromptOptions['language']): 'js' | 'ts' { 96 | return language === 'JavaScript' ? 'js' : 'ts'; 97 | } 98 | 99 | export async function readDirRecursively(dir: string): Promise { 100 | const dirents = await fs.promises.readdir(dir, { withFileTypes: true }); 101 | const files = await Promise.all(dirents.map(async (dirent): Promise => { 102 | const res = path.join(dir, dirent.name); 103 | return dirent.isDirectory() ? await readDirRecursively(res) : res; 104 | })); 105 | return files.flat(); 106 | } 107 | -------------------------------------------------------------------------------- /src/packageManager.ts: -------------------------------------------------------------------------------- 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 path from 'path'; 18 | import fs from 'fs'; 19 | 20 | export interface PackageManager { 21 | cli: string; 22 | name: string 23 | init(): string 24 | npx(command: string, args: string): string 25 | ci(): string 26 | i(): string 27 | installDevDependency(name: string): string 28 | runPlaywrightTest(args?: string): string 29 | run(script: string): string 30 | } 31 | 32 | class NPM implements PackageManager { 33 | name = 'NPM' 34 | cli = 'npm' 35 | 36 | init(): string { 37 | return 'npm init -y' 38 | } 39 | 40 | npx(command: string, args: string): string { 41 | return `npx ${command} ${args}` 42 | } 43 | 44 | ci(): string { 45 | return 'npm ci' 46 | } 47 | 48 | i(): string { 49 | return 'npm i' 50 | } 51 | 52 | installDevDependency(name: string): string { 53 | return `npm install --save-dev ${name}` 54 | } 55 | 56 | runPlaywrightTest(args: string): string { 57 | return this.npx('playwright', `test${args ? (' ' + args) : ''}`); 58 | } 59 | 60 | run(script: string): string { 61 | return `npm run ${script}`; 62 | } 63 | } 64 | 65 | class Yarn implements PackageManager { 66 | name = 'Yarn' 67 | cli = 'yarn' 68 | private workspace: boolean 69 | private classic = false; 70 | 71 | constructor(rootDir: string, version?: string) { 72 | this.workspace = this.isWorkspace(rootDir); 73 | if (version) 74 | this.classic = version.startsWith('0') || version.startsWith('1'); 75 | } 76 | 77 | private isWorkspace(rootDir: string) { 78 | try { 79 | const packageJSON = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')); 80 | return !!packageJSON.workspaces; 81 | } catch (e) { 82 | return false; 83 | } 84 | } 85 | 86 | init(): string { 87 | return 'yarn init -y' 88 | } 89 | 90 | npx(command: string, args: string): string { 91 | return `yarn ${command} ${args}` 92 | } 93 | 94 | ci(): string { 95 | return 'npm install -g yarn && yarn' 96 | } 97 | 98 | i(): string { 99 | return this.ci() 100 | } 101 | 102 | installDevDependency(name: string): string { 103 | return `yarn add --dev ${(this.workspace && this.classic) ? '-W ' : ''}${name}` 104 | } 105 | 106 | runPlaywrightTest(args: string): string { 107 | return this.npx('playwright', `test${args ? (' ' + args) : ''}`); 108 | } 109 | 110 | run(script: string): string { 111 | return `yarn ${script}`; 112 | } 113 | } 114 | 115 | class PNPM implements PackageManager { 116 | name = 'pnpm' 117 | cli = 'pnpm' 118 | private workspace: boolean; 119 | 120 | constructor(rootDir: string) { 121 | this.workspace = fs.existsSync(path.resolve(rootDir, 'pnpm-workspace.yaml')); 122 | } 123 | 124 | init(): string { 125 | return 'pnpm init' 126 | } 127 | 128 | npx(command: string, args: string): string { 129 | return `pnpm exec ${command} ${args}` 130 | } 131 | 132 | ci(): string { 133 | return 'npm install -g pnpm && pnpm install' 134 | } 135 | 136 | i(): string { 137 | return this.ci() 138 | } 139 | 140 | installDevDependency(name: string): string { 141 | return `pnpm add --save-dev ${this.workspace ? '-w ' : ''}${name}` 142 | } 143 | 144 | runPlaywrightTest(args: string): string { 145 | return this.npx('playwright', `test${args ? (' ' + args) : ''}`); 146 | } 147 | 148 | run(script: string): string { 149 | return `pnpm run ${script}`; 150 | } 151 | } 152 | 153 | export function determinePackageManager(rootDir: string): PackageManager { 154 | const userAgent = process.env.npm_config_user_agent; 155 | if (userAgent) { 156 | if (userAgent.includes('yarn')) { 157 | const yarnVersion = userAgent.match(/yarn\/(\d+\.\d+\.\d+)/)?.[1]; 158 | return new Yarn(rootDir, yarnVersion); 159 | } 160 | if (userAgent.includes('pnpm')) 161 | return new PNPM(rootDir); 162 | } 163 | return new NPM(); 164 | } 165 | -------------------------------------------------------------------------------- /tests/baseFixtures.ts: -------------------------------------------------------------------------------- 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 as base } from '@playwright/test'; 18 | import { spawn, type SpawnOptionsWithoutStdio } from 'child_process'; 19 | import path from 'path'; 20 | import fs from 'fs'; 21 | import type { PromptOptions } from '../src/generator'; 22 | 23 | export type PackageManager = 'npm' | 'pnpm' | 'pnpm-pnp' | 'yarn-classic' | 'yarn-berry'; 24 | 25 | const userAgents: Record = { 26 | 'yarn-classic': 'yarn/1.22.10', 27 | 'yarn-berry': 'yarn/4.0.0', 28 | pnpm: 'pnpm/0.0.0', 29 | 'pnpm-pnp': 'pnpm/0.0.0', 30 | npm: undefined, 31 | }; 32 | 33 | export type TestFixtures = { 34 | packageManager: PackageManager; 35 | dir: string; 36 | run: (parameters: string[], options: PromptOptions) => Promise, 37 | exec: typeof spawnAsync, 38 | }; 39 | 40 | type SpawnResult = {stdout: string, stderr: string, code: number | null, error?: Error}; 41 | 42 | function spawnAsync(cmd: string, args: string[], options?: SpawnOptionsWithoutStdio): Promise { 43 | const p = spawn(cmd, args, options); 44 | 45 | return new Promise(resolve => { 46 | let stdout = ''; 47 | let stderr = ''; 48 | if (process.env.CR_PW_DEBUG) { 49 | p.stdout.on('data', chunk => process.stdout.write(chunk)); 50 | p.stderr.on('data', chunk => process.stderr.write(chunk)); 51 | } 52 | if (p.stdout) 53 | p.stdout.on('data', data => stdout += data); 54 | if (p.stderr) 55 | p.stderr.on('data', data => stderr += data); 56 | p.on('close', code => resolve({ stdout, stderr, code })); 57 | p.on('error', error => resolve({ stdout, stderr, code: 0, error })); 58 | }); 59 | } 60 | 61 | export const test = base.extend({ 62 | packageManager: ['npm', { option: true }], 63 | dir: async ({ packageManager }, use, testInfo) => { 64 | const dir = testInfo.outputDir; 65 | fs.mkdirSync(dir, { recursive: true }); 66 | if (packageManager === 'pnpm-pnp') 67 | fs.writeFileSync(path.join(dir, '.npmrc'), 'node-linker=pnp'); 68 | await use(dir); 69 | }, 70 | exec: async ({ dir }, use, testInfo) => { 71 | await use(async (cmd: string, args: string[], options?: SpawnOptionsWithoutStdio): ReturnType => { 72 | const result = await spawnAsync(cmd, args, { 73 | cwd: dir, 74 | ...options, 75 | }); 76 | if (result.code !== 0) { 77 | throw new Error([ 78 | `Failed to run "${cmd} ${args.join(' ')}"`, 79 | `stdout:`, 80 | result.stdout, 81 | `stderr:`, 82 | result.stderr, 83 | ].join('\n')); 84 | } 85 | return result; 86 | }); 87 | }, 88 | run: async ({ packageManager, exec, dir }, use) => { 89 | await use(async (parameters: string[], options: PromptOptions): Promise => { 90 | return await exec('node', [path.join(__dirname, '..'), ...parameters], { 91 | cwd: dir, 92 | env: { 93 | ...process.env, 94 | npm_config_user_agent: userAgents[packageManager], 95 | 'TEST_OPTIONS': JSON.stringify(options), 96 | }, 97 | }); 98 | }); 99 | }, 100 | }); 101 | 102 | export function assertLockFilesExist(dir: string, packageManager: PackageManager) { 103 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 104 | if (packageManager === 'npm') 105 | expect(fs.existsSync(path.join(dir, 'package-lock.json'))).toBeTruthy(); 106 | else if (packageManager.startsWith('yarn')) 107 | expect(fs.existsSync(path.join(dir, 'yarn.lock'))).toBeTruthy(); 108 | else if (packageManager.startsWith('pnpm')) 109 | expect(fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))).toBeTruthy(); 110 | } 111 | 112 | export function packageManagerToNpxCommand(packageManager: PackageManager): string { 113 | switch (packageManager) { 114 | case 'npm': 115 | return 'npx'; 116 | case 'yarn-classic': 117 | case 'yarn-berry': 118 | return 'yarn'; 119 | case 'pnpm': 120 | case 'pnpm-pnp': 121 | return 'pnpm dlx'; 122 | } 123 | } 124 | 125 | export const expect = test.expect; 126 | -------------------------------------------------------------------------------- /tests/integration.spec.ts: -------------------------------------------------------------------------------- 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 | import fs from 'fs'; 17 | import path from 'path'; 18 | import { assertLockFilesExist, expect, packageManagerToNpxCommand, test } from './baseFixtures'; 19 | 20 | const validGitignore = [ 21 | '# Playwright', 22 | 'node_modules/', 23 | '/test-results/', 24 | '/playwright-report/', 25 | '/blob-report/', 26 | '/playwright/.cache/', 27 | '/playwright/.auth/' 28 | ].join('\n'); 29 | 30 | test('should generate a project in the current directory', async ({ run, dir, packageManager }) => { 31 | test.skip(packageManager === 'yarn-classic' || packageManager === 'yarn-berry'); 32 | test.slow(); 33 | const { stdout } = await run([], { installGitHubActions: true, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 34 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.ts'))).toBeTruthy(); 35 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 36 | assertLockFilesExist(dir, packageManager); 37 | expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy(); 38 | const playwrightConfigContent = fs.readFileSync(path.join(dir, 'playwright.config.ts'), 'utf8'); 39 | expect(playwrightConfigContent).toContain('tests'); 40 | expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); 41 | expect(fs.existsSync(path.join(dir, '.gitignore'))).toBeTruthy(); 42 | expect(fs.readFileSync(path.join(dir, '.gitignore'), { encoding: 'utf8' }).trim()).toBe(validGitignore); 43 | if (packageManager === 'npm') { 44 | expect(stdout).toContain('Initializing NPM project (npm init -y)…'); 45 | expect(stdout).toContain('Installing Playwright Test (npm install --save-dev @playwright/test)…'); 46 | expect(stdout).toContain('Installing Types (npm install --save-dev @types/node)…'); 47 | } else if (packageManager === 'yarn-classic') { 48 | expect(stdout).toContain('Initializing Yarn project (yarn init -y)…'); 49 | expect(stdout).toContain('Installing Playwright Test (yarn add --dev @playwright/test)…'); 50 | expect(stdout).toContain('Installing Types (yarn add --dev @types/node)…'); 51 | } else if (packageManager === 'pnpm' || packageManager === 'pnpm-pnp') { 52 | expect(stdout).toContain('pnpm init'); // pnpm command outputs name in different case, hence we are not testing the whole string 53 | expect(stdout).toContain('Installing Playwright Test (pnpm add --save-dev @playwright/test)…'); 54 | expect(stdout).toContain('Installing Types (pnpm add --save-dev @types/node)…'); 55 | } 56 | expect(stdout).toContain('npx playwright install' + process.platform === 'linux' ? ' --with-deps' : ''); 57 | }); 58 | 59 | test('should generate a project in a given directory', async ({ run, dir, packageManager }) => { 60 | test.skip(packageManager === 'yarn-classic' || packageManager === 'yarn-berry'); 61 | await run(['foobar'], { installGitHubActions: true, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 62 | expect(fs.existsSync(path.join(dir, 'foobar/tests/example.spec.ts'))).toBeTruthy(); 63 | expect(fs.existsSync(path.join(dir, 'foobar/package.json'))).toBeTruthy(); 64 | assertLockFilesExist(path.join(dir, 'foobar'), packageManager); 65 | expect(fs.existsSync(path.join(dir, 'foobar/playwright.config.ts'))).toBeTruthy(); 66 | expect(fs.existsSync(path.join(dir, 'foobar/.github/workflows/playwright.yml'))).toBeTruthy(); 67 | }); 68 | 69 | test('should generate a project with JavaScript and without GHA', async ({ run, dir, packageManager }) => { 70 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'JavaScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 71 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.js'))).toBeTruthy(); 72 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 73 | assertLockFilesExist(dir, packageManager); 74 | expect(fs.existsSync(path.join(dir, 'playwright.config.js'))).toBeTruthy(); 75 | expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy(); 76 | }); 77 | 78 | test('should generate be able to run TS examples successfully', async ({ run, dir, exec, packageManager }) => { 79 | test.slow(); 80 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 81 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.ts'))).toBeTruthy(); 82 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 83 | expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy(); 84 | 85 | await exec(packageManagerToNpxCommand(packageManager), ['playwright', 'install-deps']); 86 | await exec(packageManagerToNpxCommand(packageManager), ['playwright', 'test']); 87 | }); 88 | 89 | test('should generate be able to run JS examples successfully', async ({ run, dir, exec, packageManager }) => { 90 | test.slow(); 91 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'JavaScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 92 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.js'))).toBeTruthy(); 93 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 94 | expect(fs.existsSync(path.join(dir, 'playwright.config.js'))).toBeTruthy(); 95 | 96 | await exec(packageManagerToNpxCommand(packageManager), ['playwright', 'install-deps']); 97 | await exec(packageManagerToNpxCommand(packageManager), ['playwright', 'test']); 98 | }); 99 | 100 | test('should generate in the root of pnpm workspace', async ({ run, packageManager, exec }) => { 101 | test.skip(packageManager !== 'pnpm' && packageManager !== 'pnpm-pnp'); 102 | 103 | // not sure what's going wrong. removing pnpm-workspace.yaml would help, as discussed in https://github.com/pnpm/pnpm/issues/4129#issuecomment-2830362402. 104 | // but I don't understand PNPM enough to know whether that make the test meaningless. 105 | // disabling for now. 106 | test.fail(packageManager === 'pnpm-pnp', 'something is broken here'); 107 | 108 | const dir = test.info().outputDir; 109 | fs.mkdirSync(dir, { recursive: true }); 110 | await exec('pnpm', ['init'], { cwd: dir }); 111 | fs.writeFileSync(path.join(dir, 'pnpm-workspace.yaml'), `packages:\n - 'packages/*'\n`); 112 | fs.mkdirSync(path.join(dir, 'packages', 'foo'), { recursive: true }); 113 | fs.writeFileSync(path.join(dir, 'packages', 'foo', 'package.json'), `{}`); 114 | 115 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: false }); 116 | assertLockFilesExist(dir, packageManager); 117 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.ts'))).toBeTruthy(); 118 | expect(fs.existsSync(path.join(dir, 'package.json'))).toBeTruthy(); 119 | expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy(); 120 | }); 121 | 122 | test('should generate in the root of yarn workspaces', async ({ run, packageManager, exec }) => { 123 | test.skip(packageManager !== 'yarn-berry' && packageManager !== 'yarn-classic'); 124 | 125 | const dir = test.info().outputDir; 126 | fs.mkdirSync(dir, { recursive: true }); 127 | fs.writeFileSync(path.join(dir, 'package.json'), `{ 128 | "name": "yarn-monorepo", 129 | "version": "1.0.0", 130 | "private": true, 131 | "workspaces": ["packages/*"] 132 | }`); 133 | for (const pkg of ['foo', 'bar']) { 134 | const packageDir = path.join(dir, 'packages', pkg); 135 | fs.mkdirSync(packageDir, { recursive: true }); 136 | await exec(`yarn`, ['init', '-y'], { cwd: packageDir }); 137 | } 138 | await exec(`yarn`, ['install'], { cwd: dir, env: { ...process.env, YARN_ENABLE_IMMUTABLE_INSTALLS: 'false', YARN_ENABLE_HARDENED_MODE: '0' } }); 139 | 140 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: false }); 141 | assertLockFilesExist(dir, packageManager); 142 | expect(fs.existsSync(path.join(dir, 'tests/example.spec.ts'))).toBeTruthy(); 143 | const writesNodeModules = packageManager === 'yarn-classic'; 144 | expect(fs.existsSync(path.join(dir, 'node_modules/playwright'))).toBe(writesNodeModules); 145 | expect(fs.existsSync(path.join(dir, 'playwright.config.ts'))).toBeTruthy(); 146 | }); 147 | 148 | test('should not duplicate gitignore entries', async ({ run, dir }) => { 149 | fs.writeFileSync(path.join(dir, '.gitignore'), validGitignore); 150 | 151 | await run([], { installGitHubActions: false, testDir: 'tests', language: 'TypeScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: false }); 152 | expect(fs.readFileSync(path.join(dir, '.gitignore'), { encoding: 'utf8' }).trim()).toBe(validGitignore); 153 | }) 154 | 155 | test('should install with "npm ci" in GHA when using npm with package-lock enabled', async ({ dir, run, packageManager }) => { 156 | test.skip(packageManager !== 'npm'); 157 | 158 | await run([], { installGitHubActions: true, testDir: 'tests', language: 'JavaScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 159 | expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); 160 | 161 | const workflowContent = fs.readFileSync(path.join(dir, '.github/workflows/playwright.yml'), 'utf8'); 162 | expect(workflowContent).not.toContain('run: npm i'); 163 | expect(workflowContent).toContain('run: npm ci'); 164 | }); 165 | 166 | test('should install with "npm i" in GHA when using npm with package-lock disabled', async ({ dir, run, packageManager }) => { 167 | test.skip(packageManager !== 'npm'); 168 | 169 | fs.writeFileSync(path.join(dir, '.npmrc'), 'package-lock=false'); 170 | await run([], { installGitHubActions: true, testDir: 'tests', language: 'JavaScript', installPlaywrightDependencies: false, installPlaywrightBrowsers: true }); 171 | expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); 172 | 173 | const workflowContent = fs.readFileSync(path.join(dir, '.github/workflows/playwright.yml'), 'utf8'); 174 | expect(workflowContent).toContain('run: npm i'); 175 | expect(workflowContent).not.toContain('run: npm ci'); 176 | }); 177 | 178 | test('is proper yarn classic', async ({ packageManager, exec }) => { 179 | test.skip(packageManager !== 'yarn-classic'); 180 | const result = await exec('yarn --version', [], { cwd: test.info().outputDir, shell: true }); 181 | expect(result.stdout).toMatch(/^1\./); 182 | }); 183 | 184 | test('is proper yarn berry', async ({ packageManager, exec }) => { 185 | test.skip(packageManager !== 'yarn-berry'); 186 | const result = await exec('yarn --version', [], { cwd: test.info().outputDir, shell: true }); 187 | expect(result.stdout).toMatch(/^4\./); 188 | }); -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 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 | import fs from 'fs'; 17 | import path from 'path'; 18 | 19 | import colors from 'ansi-colors'; 20 | import { prompt } from 'enquirer'; 21 | import ini from 'ini'; 22 | 23 | import { type PackageManager, determinePackageManager } from './packageManager'; 24 | import { Command, createFiles, executeCommands, executeTemplate, getFileExtensionCT, languageToFileExtension } from './utils'; 25 | 26 | export type PromptOptions = { 27 | testDir: string, 28 | installGitHubActions: boolean, 29 | language: 'JavaScript' | 'TypeScript', 30 | framework?: 'react' | 'react17' | 'vue' | 'vue2' | 'svelte' | 'solid' | undefined, 31 | installPlaywrightDependencies: boolean, 32 | installPlaywrightBrowsers: boolean, 33 | }; 34 | 35 | const assetsDir = path.join(__dirname, '..', 'assets'); 36 | 37 | export type CliOptions = { 38 | lang?: string; 39 | browser?: string[]; 40 | noBrowsers?: boolean; 41 | noExamples?: boolean; 42 | installDeps?: boolean; 43 | next?: boolean; 44 | beta?: boolean; 45 | ct?: boolean; 46 | quiet?: boolean; 47 | gha?: boolean; 48 | }; 49 | 50 | export class Generator { 51 | private readonly options: CliOptions 52 | private packageManager: PackageManager; 53 | 54 | constructor(private readonly rootDir: string, options: CliOptions) { 55 | this.options = options; 56 | if (!fs.existsSync(rootDir)) 57 | fs.mkdirSync(rootDir); 58 | this.packageManager = determinePackageManager(rootDir); 59 | } 60 | 61 | async run() { 62 | this._printPrologue(); 63 | const answers = await this._askQuestions(); 64 | const { files, commands: allCommands } = await this._identifyChanges(answers); 65 | const [preCommands, postCommands] = allCommands.reduce((acc, command) => { 66 | acc[command.phase === 'pre' ? 0 : 1].push(command); 67 | return acc; 68 | }, [[] as Command[], [] as Command[]]); 69 | executeCommands(this.rootDir, preCommands); 70 | await createFiles(this.rootDir, files); 71 | this._patchGitIgnore(); 72 | await this._patchPackageJSON(answers); 73 | executeCommands(this.rootDir, postCommands); 74 | if (answers.framework) 75 | this._printEpilogueCT(); 76 | else 77 | this._printEpilogue(answers); 78 | } 79 | 80 | private _printPrologue() { 81 | console.log(colors.yellow(`Getting started with writing ${colors.bold('end-to-end')} tests with ${colors.bold('Playwright')}:`)); 82 | console.log(`Initializing project in '${path.relative(process.cwd(), this.rootDir) || '.'}'`); 83 | } 84 | 85 | private async _askQuestions(): Promise { 86 | if (process.env.TEST_OPTIONS) 87 | return JSON.parse(process.env.TEST_OPTIONS); 88 | if (this.options.quiet) { 89 | return { 90 | installGitHubActions: !!this.options.gha, 91 | language: this.options.lang === 'js' ? 'JavaScript' : 'TypeScript', 92 | installPlaywrightDependencies: !!this.options.installDeps, 93 | testDir: fs.existsSync(path.join(this.rootDir, 'tests')) ? 'e2e' : 'tests', 94 | framework: undefined, 95 | installPlaywrightBrowsers: !this.options.noBrowsers, 96 | }; 97 | } 98 | 99 | const isDefinitelyTS = fs.existsSync(path.join(this.rootDir, 'tsconfig.json')); 100 | 101 | const questions = [ 102 | !isDefinitelyTS && { 103 | type: 'select', 104 | name: 'language', 105 | message: 'Do you want to use TypeScript or JavaScript?', 106 | choices: [ 107 | { name: 'TypeScript' }, 108 | { name: 'JavaScript' }, 109 | ], 110 | initial: this.options.lang === 'js' ? 'JavaScript' : 'TypeScript', 111 | skip: !!this.options.lang, 112 | }, 113 | this.options.ct && { 114 | type: 'select', 115 | name: 'framework', 116 | message: 'Which framework do you use? (experimental)', 117 | choices: [ 118 | { name: 'react', message: 'React 18' }, 119 | { name: 'react17', message: 'React 17' }, 120 | { name: 'vue', message: 'Vue 3' }, 121 | { name: 'vue2', message: 'Vue 2' }, 122 | { name: 'svelte', message: 'Svelte' }, 123 | { name: 'solid', message: 'Solid' }, 124 | ], 125 | }, 126 | !this.options.ct && { 127 | type: 'text', 128 | name: 'testDir', 129 | message: 'Where to put your end-to-end tests?', 130 | initial: fs.existsSync(path.join(this.rootDir, 'tests')) ? 'e2e' : 'tests', 131 | }, 132 | !this.options.ct && { 133 | type: 'confirm', 134 | name: 'installGitHubActions', 135 | message: 'Add a GitHub Actions workflow?', 136 | initial: true, 137 | skip: !!this.options.gha, 138 | }, 139 | { 140 | type: 'confirm', 141 | name: 'installPlaywrightBrowsers', 142 | message: `Install Playwright browsers (can be done manually via '${this.packageManager.npx('playwright', 'install')}')?`, 143 | initial: !this.options.noBrowsers || !!this.options.browser, 144 | skip: !!this.options.noBrowsers || !!this.options.browser, 145 | }, 146 | // Avoid installing dependencies on Windows (vast majority does not run create-playwright on Windows) 147 | // Avoid installing dependencies on Mac (there are no dependencies) 148 | process.platform === 'linux' && { 149 | type: 'confirm', 150 | name: 'installPlaywrightDependencies', 151 | message: `Install Playwright operating system dependencies (requires sudo / root - can be done manually via 'sudo ${this.packageManager.npx('playwright', 'install-deps')}')?`, 152 | initial: !!this.options.installDeps, 153 | skip: !!this.options.installDeps, 154 | }, 155 | ]; 156 | const result = await prompt( 157 | questions.filter(Boolean) as Exclude< 158 | (typeof questions)[number], 159 | boolean | undefined 160 | >[], 161 | ); 162 | if (isDefinitelyTS) 163 | result.language = 'TypeScript'; 164 | return result; 165 | } 166 | 167 | private async _identifyChanges(answers: PromptOptions) { 168 | const commands: (Command & { phase: 'pre' | 'post' })[] = []; 169 | const files = new Map(); 170 | const fileExtension = languageToFileExtension(answers.language); 171 | 172 | const sections = new Map(); 173 | for (const browserName of ['chromium', 'firefox', 'webkit']) 174 | sections.set(browserName, !this.options.browser || this.options.browser.includes(browserName) ? 'show' : 'comment'); 175 | 176 | let ctPackageName; 177 | let installExamples = !this.options.noExamples; 178 | if (answers.framework) { 179 | ctPackageName = `@playwright/experimental-ct-${answers.framework}`; 180 | installExamples = false; 181 | files.set(`playwright-ct.config.${fileExtension}`, executeTemplate(this._readAsset(`playwright-ct.config.${fileExtension}`), { 182 | testDir: answers.testDir || '', 183 | ctPackageName, 184 | }, sections)); 185 | } else { 186 | files.set(`playwright.config.${fileExtension}`, executeTemplate(this._readAsset(`playwright.config.${fileExtension}`), { 187 | testDir: answers.testDir || '', 188 | }, sections)); 189 | } 190 | 191 | if (answers.installGitHubActions) { 192 | const npmrcExists = fs.existsSync(path.join(this.rootDir, '.npmrc')); 193 | const packageLockDisabled = npmrcExists && ini.parse(fs.readFileSync(path.join(this.rootDir, '.npmrc'), 'utf-8'))['package-lock'] === false; 194 | const githubActionsScript = executeTemplate(this._readAsset('github-actions.yml'), { 195 | installDepsCommand: packageLockDisabled ? this.packageManager.i() : this.packageManager.ci(), 196 | installPlaywrightCommand: this.packageManager.npx('playwright', 'install --with-deps'), 197 | runTestsCommand: answers.framework ? this.packageManager.run('test-ct') : this.packageManager.runPlaywrightTest(), 198 | }, new Map()); 199 | files.set('.github/workflows/playwright.yml', githubActionsScript); 200 | } 201 | 202 | if (installExamples) 203 | files.set(path.join(answers.testDir, `example.spec.${fileExtension}`), this._readAsset(`example.spec.${fileExtension}`)); 204 | 205 | if (!fs.existsSync(path.join(this.rootDir, 'package.json'))) { 206 | commands.push({ 207 | name: `Initializing ${this.packageManager.name} project`, 208 | command: this.packageManager.init(), 209 | phase: 'pre', 210 | }); 211 | } 212 | 213 | let packageTag = ''; 214 | if (this.options.beta) 215 | packageTag = '@beta'; 216 | if (this.options.next) 217 | packageTag = '@next'; 218 | 219 | if (!this.options.ct) { 220 | commands.push({ 221 | name: 'Installing Playwright Test', 222 | command: this.packageManager.installDevDependency(`@playwright/test${packageTag}`), 223 | phase: 'pre', 224 | }); 225 | } 226 | 227 | if (this.options.ct) { 228 | commands.push({ 229 | name: 'Installing Playwright Component Testing', 230 | command: this.packageManager.installDevDependency(`${ctPackageName}${packageTag}`), 231 | phase: 'pre', 232 | }); 233 | 234 | const extension = getFileExtensionCT(answers.language, answers.framework); 235 | const htmlTemplate = executeTemplate(this._readAsset(path.join('playwright', 'index.html')), { extension }, new Map()); 236 | files.set('playwright/index.html', htmlTemplate); 237 | 238 | const jsTemplate = this._readAsset(path.join('playwright', 'index.js')); 239 | files.set(`playwright/index.${extension}`, jsTemplate); 240 | } 241 | 242 | if (!this._hasDependency('@types/node')) { 243 | commands.push({ 244 | name: 'Installing Types', 245 | command: this.packageManager.installDevDependency(`@types/node`), 246 | phase: 'pre', 247 | }); 248 | } 249 | 250 | const browsersSuffix = this.options.browser ? ' ' + this.options.browser.join(' ') : ''; 251 | if (answers.installPlaywrightBrowsers) { 252 | commands.push({ 253 | name: 'Downloading browsers', 254 | command: this.packageManager.npx('playwright', 'install') + (answers.installPlaywrightDependencies ? ' --with-deps' : '') + browsersSuffix, 255 | phase: 'post', 256 | }); 257 | } 258 | 259 | return { files, commands }; 260 | } 261 | 262 | private _hasDependency(pkg: string) { 263 | try { 264 | const packageJSON = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf-8')); 265 | return packageJSON.dependencies?.[pkg] || packageJSON.devDependencies?.[pkg] || packageJSON.optionalDependencies?.[pkg]; 266 | } catch (e) { 267 | return false; 268 | } 269 | } 270 | 271 | private _patchGitIgnore() { 272 | const gitIgnorePath = path.join(this.rootDir, '.gitignore'); 273 | let gitIgnore = ''; 274 | if (fs.existsSync(gitIgnorePath)) 275 | gitIgnore = fs.readFileSync(gitIgnorePath, 'utf-8').trimEnd() + '\n'; 276 | 277 | let thisIsTheFirstLineWeAreAdding = true; 278 | const valuesToAdd = { 279 | 'node_modules/': /^node_modules\/?/m, 280 | '/test-results/': /^\/?test-results\/?$/m, 281 | '/playwright-report/': /^\/playwright-report\/?$/m, 282 | '/blob-report/': /^\/blob-report\/?$/m, 283 | '/playwright/.cache/': /^\/playwright\/\.cache\/?$/m, 284 | '/playwright/.auth/': /^\/playwright\/\.auth\/?$/m, 285 | }; 286 | Object.entries(valuesToAdd).forEach(([value, regex]) => { 287 | if (!gitIgnore.match(regex)) { 288 | if (thisIsTheFirstLineWeAreAdding) { 289 | gitIgnore += `\n# Playwright\n`; 290 | thisIsTheFirstLineWeAreAdding = false; 291 | } 292 | 293 | gitIgnore += `${value}\n`; 294 | } 295 | }); 296 | fs.writeFileSync(gitIgnorePath, gitIgnore); 297 | } 298 | 299 | private _readAsset(asset: string): string { 300 | return fs.readFileSync(path.isAbsolute(asset) ? asset : path.join(assetsDir, asset), 'utf-8'); 301 | } 302 | 303 | private async _patchPackageJSON(answers: PromptOptions) { 304 | const packageJSON = JSON.parse(fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf-8')); 305 | if (!packageJSON.scripts) 306 | packageJSON.scripts = {}; 307 | if (packageJSON.scripts['test']?.includes('no test specified')) 308 | delete packageJSON.scripts['test']; 309 | 310 | const extension = languageToFileExtension(answers.language); 311 | if (answers.framework) 312 | packageJSON.scripts['test-ct'] = `playwright test -c playwright-ct.config.${extension}`; 313 | 314 | const files = new Map(); 315 | files.set('package.json', JSON.stringify(packageJSON, null, 2) + '\n'); // NPM keeps a trailing new-line 316 | await createFiles(this.rootDir, files, true); 317 | } 318 | 319 | private _printEpilogue(answers: PromptOptions) { 320 | console.log(colors.green('✔ Success!') + ' ' + colors.bold(`Created a Playwright Test project at ${this.rootDir}`)); 321 | const pathToNavigate = path.relative(process.cwd(), this.rootDir); 322 | const prefix = pathToNavigate !== '' ? ` cd ${pathToNavigate}\n` : ''; 323 | const exampleSpecPath = path.join(answers.testDir, `example.spec.${languageToFileExtension(answers.language)}`); 324 | const playwrightConfigPath = `playwright.config.${languageToFileExtension(answers.language)}`; 325 | console.log(` 326 | Inside that directory, you can run several commands: 327 | 328 | ${colors.cyan(this.packageManager.runPlaywrightTest())} 329 | Runs the end-to-end tests. 330 | 331 | ${colors.cyan(this.packageManager.runPlaywrightTest('--ui'))} 332 | Starts the interactive UI mode. 333 | 334 | ${colors.cyan(this.packageManager.runPlaywrightTest('--project=chromium'))} 335 | Runs the tests only on Desktop Chrome. 336 | 337 | ${colors.cyan(this.packageManager.runPlaywrightTest('example'))} 338 | Runs the tests in a specific file. 339 | 340 | ${colors.cyan(this.packageManager.runPlaywrightTest('--debug'))} 341 | Runs the tests in debug mode. 342 | 343 | ${colors.cyan(this.packageManager.npx('playwright', 'codegen'))} 344 | Auto generate tests with Codegen. 345 | 346 | We suggest that you begin by typing: 347 | 348 | ${colors.cyan(prefix + ' ' + this.packageManager.runPlaywrightTest())} 349 | 350 | And check out the following files: 351 | - .${path.sep}${pathToNavigate ? path.join(pathToNavigate, exampleSpecPath) : exampleSpecPath} - Example end-to-end test 352 | - .${path.sep}${pathToNavigate ? path.join(pathToNavigate, playwrightConfigPath) : playwrightConfigPath} - Playwright Test configuration 353 | 354 | Visit https://playwright.dev/docs/intro for more information. ✨ 355 | 356 | Happy hacking! 🎭`); 357 | } 358 | 359 | private _printEpilogueCT() { 360 | console.log(colors.green('✔ Success!') + ' ' + colors.bold(`Created a Playwright Test project at ${this.rootDir}`)); 361 | console.log(` 362 | Inside that directory, you can run several commands: 363 | 364 | ${colors.cyan(`${this.packageManager.cli} run test-ct`)} 365 | Runs the component tests. 366 | 367 | ${colors.cyan(`${this.packageManager.cli} run test-ct -- --project=chromium`)} 368 | Runs the tests only on Desktop Chrome. 369 | 370 | ${colors.cyan(`${this.packageManager.cli} run test-ct App.test.ts`)} 371 | Runs the tests in the specific file. 372 | 373 | ${colors.cyan(`${this.packageManager.cli} run test-ct -- --debug`)} 374 | Runs the tests in debug mode. 375 | 376 | We suggest that you begin by typing: 377 | 378 | ${colors.cyan(`${this.packageManager.cli} run test-ct`)} 379 | 380 | Visit https://playwright.dev/docs/intro for more information. ✨ 381 | 382 | Happy hacking! 🎭`); 383 | } 384 | } 385 | --------------------------------------------------------------------------------