├── .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 | [](https://www.npmjs.com/package/create-playwright)
6 | [](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 |
--------------------------------------------------------------------------------