From 9c6ab6ea829a1451a55291d4055a6dbb9aceb810 Mon Sep 17 00:00:00 2001 From: Karl Horky Date: Thu, 2 Apr 2026 17:32:36 +0200 Subject: [PATCH 1/4] fix: skip gha prompt when explicitly set --- src/cli.ts | 1 + src/generator.ts | 6 +++--- tests/baseFixtures.ts | 6 +++--- tests/integration.spec.ts | 26 +++++++++++++++++++++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 11c73a2..be9da0b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,6 +33,7 @@ program .option('--ct', 'install Playwright Component testing') .option('--quiet', 'do not ask for interactive input prompts') .option('--gha', 'install GitHub Actions') + .option('--no-gha', 'do not install GitHub Actions') .option('--lang ', 'language to use (js, TypeScript)') .option('--test-dir ', 'directory for test files (default: "tests" if it does not exist, otherwise "e2e")') .action(async (rootDir, options) => { diff --git a/src/generator.ts b/src/generator.ts index df55b88..b557f28 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -111,7 +111,7 @@ export class Generator { { name: 'TypeScript' }, { name: 'JavaScript' }, ], - initial: this.options.lang === 'js' ? 'JavaScript' : 'TypeScript', + initial: this.options.lang === 'js' ? 1 : 0, skip: !!this.options.lang, }, this.options.ct && { @@ -138,8 +138,8 @@ export class Generator { type: 'confirm', name: 'installGitHubActions', message: 'Add a GitHub Actions workflow?', - initial: true, - skip: !!this.options.gha, + initial: this.options.gha ?? true, + skip: this.options.gha !== undefined, }, { type: 'confirm', diff --git a/tests/baseFixtures.ts b/tests/baseFixtures.ts index 23c4843..64a0ce2 100644 --- a/tests/baseFixtures.ts +++ b/tests/baseFixtures.ts @@ -33,7 +33,7 @@ const userAgents: Record = { export type TestFixtures = { packageManager: PackageManager; dir: string; - run: (parameters: string[], options: PromptOptions) => Promise, + run: (parameters: string[], options?: PromptOptions) => Promise, exec: typeof spawnAsync, }; @@ -86,13 +86,13 @@ export const test = base.extend({ }); }, run: async ({ packageManager, exec, dir }, use) => { - await use(async (parameters: string[], options: PromptOptions): Promise => { + await use(async (parameters: string[], options?: PromptOptions): Promise => { return await exec('node', [path.join(__dirname, '..'), ...parameters], { cwd: dir, env: { ...process.env, npm_config_user_agent: userAgents[packageManager], - 'TEST_OPTIONS': JSON.stringify(options), + ...(options ? { TEST_OPTIONS: JSON.stringify(options) } : {}), }, }); }); diff --git a/tests/integration.spec.ts b/tests/integration.spec.ts index 995a67d..4c57f88 100644 --- a/tests/integration.spec.ts +++ b/tests/integration.spec.ts @@ -187,6 +187,30 @@ test('should not prompt and skip existing files in --quiet mode', async ({ run, expect(fs.readFileSync(path.join(dir, 'playwright.config.ts'), 'utf8')).toBe(originalConfig); }); +test('should skip GHA prompt and install workflow when --gha is passed', async ({ run, dir }) => { + await run(['--quiet', '--gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers']); + + expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); + expect(fs.existsSync(path.join(dir, 'specs/example.spec.js'))).toBeTruthy(); + expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); +}); + +test('should skip GHA prompt and not install workflow when --no-gha is passed', async ({ run, dir }) => { + await run(['--quiet', '--no-gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers']); + + expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy(); + expect(fs.existsSync(path.join(dir, 'specs/example.spec.js'))).toBeTruthy(); + expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); +}); + +test('should skip GHA prompt without --quiet when --no-gha is passed', async ({ run, dir }) => { + await run(['--no-gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers', '--install-deps']); + + expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy(); + expect(fs.existsSync(path.join(dir, 'specs/example.spec.js'))).toBeTruthy(); + expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); +}); + test('is proper yarn classic', async ({ packageManager, exec }) => { test.skip(packageManager !== 'yarn-classic'); const result = await exec('yarn --version', [], { cwd: test.info().outputDir, shell: true }); @@ -197,4 +221,4 @@ test('is proper yarn berry', async ({ packageManager, exec }) => { test.skip(packageManager !== 'yarn-berry'); const result = await exec('yarn --version', [], { cwd: test.info().outputDir, shell: true }); expect(result.stdout).toMatch(/^4\./); -}); \ No newline at end of file +}); From 6b9683147f4d9a38cdeade2a104ee97e14e863d2 Mon Sep 17 00:00:00 2001 From: Karl Horky Date: Thu, 2 Apr 2026 18:12:24 +0200 Subject: [PATCH 2/4] Switch from `initial` and `skip` to dynamic `skip()` function --- src/generator.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index b557f28..20dc72b 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -101,6 +101,7 @@ export class Generator { } const isDefinitelyTS = fs.existsSync(path.join(this.rootDir, 'tsconfig.json')); + const cliOptions = this.options; const questions = [ !isDefinitelyTS && { @@ -111,8 +112,12 @@ export class Generator { { name: 'TypeScript' }, { name: 'JavaScript' }, ], - initial: this.options.lang === 'js' ? 1 : 0, - skip: !!this.options.lang, + skip(this: { index: number }) { + if (!cliOptions.lang) + return false; + this.index = cliOptions.lang === 'js' ? 1 : 0; + return true; + }, }, this.options.ct && { type: 'select', From 49fac65a889d3175ae777157c1b87be9941f04f4 Mon Sep 17 00:00:00 2001 From: Karl Horky Date: Tue, 7 Apr 2026 22:55:39 +0200 Subject: [PATCH 3/4] Switch to quiet automatically on non-TTY --- src/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/generator.ts b/src/generator.ts index 20dc72b..17e5ba5 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -89,7 +89,7 @@ export class Generator { const testDir = this.options.testDir || (fs.existsSync(path.join(this.rootDir, 'tests')) ? 'e2e' : 'tests'); - if (this.options.quiet) { + if (this.options.quiet || !process.stdin.isTTY) { return { installGitHubActions: !!this.options.gha, language: this.options.lang === 'js' ? 'JavaScript' : 'TypeScript', From d1ab4a85a793abba90f6cc2b5fb5bdd33205eeb6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 7 Oct 2026 19:39:28 -0700 Subject: [PATCH 4/4] chore: address review feedback - Do not ask the language question when --lang is passed instead of setting the select index from within enquirer's skip(). - Treat a non-TTY stdin as --quiet in the CLI, so existing files are not silently overwritten in non-interactive terminals. - Test the prompt path in-process: spawned CLI runs have no TTY and always take the quiet path. - Rename the GHA tests and drop --install-deps from the non-TTY test. --- src/cli.ts | 3 ++- src/generator.ts | 13 ++++--------- tests/generator.spec.ts | 40 +++++++++++++++++++++++++++++++++++++++ tests/integration.spec.ts | 15 +++++++++++---- 4 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 tests/generator.spec.ts diff --git a/src/cli.ts b/src/cli.ts index 581e193..5cb8f27 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -44,7 +44,8 @@ program installDeps: options.installDeps, next: options.next, beta: options.beta, - quiet: options.quiet, + // Prompts cannot be answered without a TTY, fall back to the --quiet defaults. + quiet: options.quiet || !process.stdin.isTTY, gha: options.gha, lang: options.lang, testDir: options.testDir, diff --git a/src/generator.ts b/src/generator.ts index c93e99a..52e2d8b 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -84,7 +84,7 @@ export class Generator { const testDir = this.options.testDir || (fs.existsSync(path.join(this.rootDir, 'tests')) ? 'e2e' : 'tests'); - if (this.options.quiet || !process.stdin.isTTY) { + if (this.options.quiet) { return { installGitHubActions: !!this.options.gha, language: this.options.lang === 'js' ? 'JavaScript' : 'TypeScript', @@ -95,10 +95,9 @@ export class Generator { } const isDefinitelyTS = fs.existsSync(path.join(this.rootDir, 'tsconfig.json')); - const cliOptions = this.options; const questions = [ - !isDefinitelyTS && { + !isDefinitelyTS && !this.options.lang && { type: 'select', name: 'language', message: 'Do you want to use TypeScript or JavaScript?', @@ -106,12 +105,6 @@ export class Generator { { name: 'TypeScript' }, { name: 'JavaScript' }, ], - skip(this: { index: number }) { - if (!cliOptions.lang) - return false; - this.index = cliOptions.lang === 'js' ? 1 : 0; - return true; - }, }, { type: 'text', @@ -152,6 +145,8 @@ export class Generator { ); if (isDefinitelyTS) result.language = 'TypeScript'; + else if (this.options.lang) + result.language = this.options.lang === 'js' ? 'JavaScript' : 'TypeScript'; return result; } diff --git a/tests/generator.spec.ts b/tests/generator.spec.ts new file mode 100644 index 0000000..2dd4079 --- /dev/null +++ b/tests/generator.spec.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from './baseFixtures'; +import { Generator } from '../src/generator'; + +// The CLI falls back to --quiet without a TTY, so the interactive path is exercised in-process. +// A question that is not skipped would wait for input and time out the test. + +test('should not prompt for --lang js and --no-gha', async ({ dir }) => { + const generator = new Generator(dir, { lang: 'js', gha: false, testDir: 'specs', noBrowsers: true, installDeps: true }); + expect(await generator['_askQuestions']()).toMatchObject({ + language: 'JavaScript', + installGitHubActions: false, + testDir: 'specs', + installPlaywrightBrowsers: false, + }); +}); + +test('should not prompt for --lang ts and --gha', async ({ dir }) => { + const generator = new Generator(dir, { lang: 'ts', gha: true, testDir: 'specs', noBrowsers: true, installDeps: true }); + expect(await generator['_askQuestions']()).toMatchObject({ + language: 'TypeScript', + installGitHubActions: true, + testDir: 'specs', + installPlaywrightBrowsers: false, + }); +}); diff --git a/tests/integration.spec.ts b/tests/integration.spec.ts index 875c9d8..9e019b2 100644 --- a/tests/integration.spec.ts +++ b/tests/integration.spec.ts @@ -231,7 +231,7 @@ test('should not prompt and skip existing files in --quiet mode', async ({ run, expect(fs.readFileSync(path.join(dir, 'playwright.config.ts'), 'utf8')).toBe(originalConfig); }); -test('should skip GHA prompt and install workflow when --gha is passed', async ({ run, dir }) => { +test('should install GHA workflow when --gha is passed', async ({ run, dir }) => { await run(['--quiet', '--gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers']); expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeTruthy(); @@ -239,7 +239,7 @@ test('should skip GHA prompt and install workflow when --gha is passed', async ( expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); }); -test('should skip GHA prompt and not install workflow when --no-gha is passed', async ({ run, dir }) => { +test('should not install GHA workflow when --no-gha is passed', async ({ run, dir }) => { await run(['--quiet', '--no-gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers']); expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy(); @@ -247,14 +247,21 @@ test('should skip GHA prompt and not install workflow when --no-gha is passed', expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); }); -test('should skip GHA prompt without --quiet when --no-gha is passed', async ({ run, dir }) => { - await run(['--no-gha', '--lang', 'js', '--test-dir', 'specs', '--no-browsers', '--install-deps']); +test('should not prompt in non-interactive terminals without --quiet', async ({ run, dir }) => { + await run(['--lang', 'js', '--test-dir', 'specs', '--no-browsers']); expect(fs.existsSync(path.join(dir, '.github/workflows/playwright.yml'))).toBeFalsy(); expect(fs.existsSync(path.join(dir, 'specs/example.spec.js'))).toBeTruthy(); expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toContain('specs'); }); +test('should not overwrite existing files in non-interactive terminals without --quiet', async ({ run, dir }) => { + fs.writeFileSync(path.join(dir, 'playwright.config.js'), '// existing'); + + await expect(run(['--lang', 'js', '--no-browsers'])).rejects.toThrowError('run again with --force'); + expect(fs.readFileSync(path.join(dir, 'playwright.config.js'), 'utf8')).toBe('// existing'); +}); + test('is proper yarn classic', async ({ packageManager, exec }) => { test.skip(packageManager !== 'yarn-classic'); const result = await exec('yarn --version', [], { cwd: test.info().outputDir, shell: true });