diff --git a/src/cli/config-flags.ts b/src/cli/config-flags.ts index 72c328e7529..3e675e9ace0 100644 --- a/src/cli/config-flags.ts +++ b/src/cli/config-flags.ts @@ -16,7 +16,6 @@ export const BOOLEAN_CLI_FLAGS = [ 'e2e', 'es5', 'esm', - 'headless', 'help', 'log', 'open', @@ -172,6 +171,21 @@ export const STRING_ARRAY_CLI_FLAGS = [ */ export const STRING_NUMBER_CLI_FLAGS = ['maxWorkers'] as const; +/** + * All the CLI arguments which may have boolean or string values. + */ +export const BOOLEAN_STRING_CLI_FLAGS = [ + /** + * `headless` is an argument passed through to Puppeteer (which is passed to Chrome) for end-to-end testing. + * Prior to Chrome v112, `headless` was treated like a boolean flag. Starting with Chrome v112, 'new' is an accepted + * option to support Chrome's new headless mode. In order to support this option in Stencil, both the boolean and + * string versions of the flag must be accepted. + * + * {@see https://developer.chrome.com/articles/new-headless/} + */ + 'headless', +] as const; + /** * All the LogLevel-type options supported by the Stencil CLI * @@ -193,6 +207,7 @@ export type StringCLIFlag = ArrayValuesAsUnion; export type StringArrayCLIFlag = ArrayValuesAsUnion; export type NumberCLIFlag = ArrayValuesAsUnion; export type StringNumberCLIFlag = ArrayValuesAsUnion; +export type BooleanStringCLIFlag = ArrayValuesAsUnion; export type LogCLIFlag = ArrayValuesAsUnion; export type KnownCLIFlag = @@ -201,6 +216,7 @@ export type KnownCLIFlag = | StringArrayCLIFlag | NumberCLIFlag | StringNumberCLIFlag + | BooleanStringCLIFlag | LogCLIFlag; type AliasMap = Partial>; @@ -275,6 +291,12 @@ type NumberConfigFlags = ObjectFromKeys; */ type StringNumberConfigFlags = ObjectFromKeys; +/** + * Type containing the configuration flags which may be set to either string + * or boolean values. + */ +type BooleanStringConfigFlags = ObjectFromKeys; + /** * Type containing the possible LogLevel configuration flags, to be included * in ConfigFlags, below @@ -301,6 +323,7 @@ export interface ConfigFlags StringArrayConfigFlags, NumberConfigFlags, StringNumberConfigFlags, + BooleanStringConfigFlags, LogLevelFlags { task: TaskCommand | null; args: string[]; diff --git a/src/cli/parse-flags.ts b/src/cli/parse-flags.ts index f300478add6..01062b507fd 100644 --- a/src/cli/parse-flags.ts +++ b/src/cli/parse-flags.ts @@ -3,6 +3,7 @@ import { readOnlyArrayHasStringMember, toCamelCase } from '@utils'; import { LOG_LEVELS, LogLevel, TaskCommand } from '../declarations'; import { BOOLEAN_CLI_FLAGS, + BOOLEAN_STRING_CLI_FLAGS, CLI_FLAG_ALIASES, CLI_FLAG_REGEX, ConfigFlags, @@ -112,6 +113,14 @@ const parseCLITerm = (flags: ConfigFlags, args: string[]) => { // array is empty, we're done! if (arg === undefined) return; + // capture whether this is a special case of a negated boolean or boolean-string before we start to test each case + const isNegatedBoolean = + !readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeFlagName(arg)) && + readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeNegativeFlagName(arg)); + const isNegatedBooleanOrString = + !readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeFlagName(arg)) && + readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizeNegativeFlagName(arg)); + // EqualsArg → "--" ArgName "=" CLIValue ; if (arg.startsWith('--') && arg.includes('=')) { // we're dealing with an EqualsArg, we have a special helper for that @@ -141,11 +150,7 @@ const parseCLITerm = (flags: ConfigFlags, args: string[]) => { } // NegativeArg → "--no" ArgName ; - else if ( - arg.startsWith('--no') && - !readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeFlagName(arg)) && - readOnlyArrayHasStringMember(BOOLEAN_CLI_FLAGS, normalizeNegativeFlagName(arg)) - ) { + else if (arg.startsWith('--no') && (isNegatedBoolean || isNegatedBooleanOrString)) { // possibly dealing with a `NegativeArg` here. There is a little ambiguity // here because we have arguments that already begin with `no` like // `notify`, so we need to test if a normalized form of the raw argument is @@ -300,6 +305,21 @@ const setCLIArg = (flags: ConfigFlags, rawArg: string, normalizedArg: string, va } } + // We're setting a value which could be either a boolean _or_ a string + else if (readOnlyArrayHasStringMember(BOOLEAN_STRING_CLI_FLAGS, normalizedArg)) { + const derivedValue = + typeof value === 'string' + ? value + ? value // use the supplied value if it's a non-empty string + : false // otherwise, default to false for the empty string + : true; // no value was supplied, default to true + flags[normalizedArg] = derivedValue; + flags.knownArgs.push(rawArg); + if (typeof derivedValue === 'string' && derivedValue) { + flags.knownArgs.push(derivedValue); + } + } + // We're setting the log level, which can only be a set of specific string values else if (readOnlyArrayHasStringMember(LOG_LEVEL_CLI_FLAGS, normalizedArg)) { if (typeof value === 'string') { diff --git a/src/cli/test/parse-flags.spec.ts b/src/cli/test/parse-flags.spec.ts index b2cc75a1533..0920ca95307 100644 --- a/src/cli/test/parse-flags.spec.ts +++ b/src/cli/test/parse-flags.spec.ts @@ -3,6 +3,8 @@ import { toDashCase } from '@utils'; import { LogLevel } from '../../declarations'; import { BOOLEAN_CLI_FLAGS, + BOOLEAN_STRING_CLI_FLAGS, + BooleanStringCLIFlag, ConfigFlags, NUMBER_CLI_FLAGS, STRING_ARRAY_CLI_FLAGS, @@ -132,6 +134,45 @@ describe('parseFlags', () => { expect(flags.config).toBe('/config-2.js'); }); + describe.each(BOOLEAN_STRING_CLI_FLAGS)('boolean-string flag - %s', (cliArg: BooleanStringCLIFlag) => { + it('parses a boolean-string flag as a boolean with no arg', () => { + const args = [`--${cliArg}`]; + const flags = parseFlags(args); + expect(flags.headless).toBe(true); + expect(flags.knownArgs).toEqual([`--${cliArg}`]); + }); + + it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no-${cliArg}`, () => { + const args = [`--no-${cliArg}`]; + const flags = parseFlags(args); + expect(flags.headless).toBe(false); + expect(flags.knownArgs).toEqual([`--no-${cliArg}`]); + }); + + it(`parses a boolean-string flag as a falsy boolean with "no" arg - --no${ + cliArg.charAt(0).toUpperCase() + cliArg.slice(1) + }`, () => { + const negativeFlag = '--no' + cliArg.charAt(0).toUpperCase() + cliArg.slice(1); + const flags = parseFlags([negativeFlag]); + expect(flags.headless).toBe(false); + expect(flags.knownArgs).toEqual([negativeFlag]); + }); + + it('parses a boolean-string flag as a string with a string arg', () => { + const args = [`--${cliArg}`, 'new']; + const flags = parseFlags(args); + expect(flags.headless).toBe('new'); + expect(flags.knownArgs).toEqual(['--headless', 'new']); + }); + + it('parses a boolean-string flag as a string with a string arg using equality', () => { + const args = [`--${cliArg}=new`]; + const flags = parseFlags(args); + expect(flags.headless).toBe('new'); + expect(flags.knownArgs).toEqual([`--${cliArg}`, 'new']); + }); + }); + describe.each(['info', 'warn', 'error', 'debug'])('logLevel %s', (level) => { it("should parse '--logLevel %s'", () => { const args = ['--logLevel', level]; diff --git a/src/compiler/config/test/validate-testing.spec.ts b/src/compiler/config/test/validate-testing.spec.ts index 129274795c9..16bdd537756 100644 --- a/src/compiler/config/test/validate-testing.spec.ts +++ b/src/compiler/config/test/validate-testing.spec.ts @@ -31,28 +31,55 @@ describe('validateTesting', () => { ]; }); - it('set headless false w/ flag', () => { - userConfig.flags = { ...flags, e2e: true, headless: false }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(false); - }); + describe('browserHeadless', () => { + describe("using 'headless' value from cli", () => { + it.each([false, true, 'new'])('sets browserHeadless to %s', (headless) => { + userConfig.flags = { ...flags, e2e: true, headless }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(headless); + }); - it('set headless true w/ flag', () => { - userConfig.flags = { ...flags, e2e: true, headless: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(true); - }); + it('defaults to true outside of CI', () => { + userConfig.flags = { ...flags, e2e: true }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(true); + }); + }); - it('default headless true', () => { - userConfig.flags = { ...flags, e2e: true }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(true); - }); + describe('with ci enabled', () => { + it("forces using the old headless mode when 'headless: false'", () => { + userConfig.flags = { ...flags, ci: true, e2e: true, headless: false }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(true); + }); - it('force headless with ci flag', () => { - userConfig.flags = { ...flags, ci: true, e2e: true, headless: false }; - const { config } = validateConfig(userConfig, mockLoadConfigInit()); - expect(config.testing.browserHeadless).toBe(true); + it('allows the new headless mode to be used', () => { + userConfig.flags = { ...flags, ci: true, e2e: true, headless: 'new' }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe('new'); + }); + }); + + describe('`testing` configuration', () => { + beforeEach(() => { + userConfig.flags = { ...flags, e2e: true, headless: undefined }; + }); + + it.each([false, true, 'new'])( + 'uses %s browserHeadless mode from testing config', + (browserHeadlessValue) => { + userConfig.testing = { browserHeadless: browserHeadlessValue }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(browserHeadlessValue); + } + ); + + it('defaults the headless mode to true when browserHeadless is not provided', () => { + userConfig.testing = {}; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + expect(config.testing.browserHeadless).toBe(true); + }); + }); }); it('default to no-sandbox browser args with ci flag', () => { diff --git a/src/compiler/config/validate-testing.ts b/src/compiler/config/validate-testing.ts index c4f8bd8222b..a6e77c687b4 100644 --- a/src/compiler/config/validate-testing.ts +++ b/src/compiler/config/validate-testing.ts @@ -20,9 +20,9 @@ export const validateTesting = (config: d.ValidatedConfig, diagnostics: d.Diagno configPathDir = config.rootDir!; } - if (typeof config.flags.headless === 'boolean') { + if (typeof config.flags.headless === 'boolean' || config.flags.headless === 'new') { testing.browserHeadless = config.flags.headless; - } else if (typeof testing.browserHeadless !== 'boolean') { + } else if (typeof testing.browserHeadless !== 'boolean' && testing.browserHeadless !== 'new') { testing.browserHeadless = true; } @@ -38,7 +38,7 @@ export const validateTesting = (config: d.ValidatedConfig, diagnostics: d.Diagno addTestingConfigOption(testing.browserArgs, '--no-sandbox'); addTestingConfigOption(testing.browserArgs, '--disable-setuid-sandbox'); addTestingConfigOption(testing.browserArgs, '--disable-dev-shm-usage'); - testing.browserHeadless = true; + testing.browserHeadless = testing.browserHeadless === 'new' ? 'new' : true; } if (typeof testing.rootDir === 'string') { diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index a646ac178dc..37a428e7c08 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -1775,9 +1775,18 @@ export interface TestingConfig extends JestConfig { browserWSEndpoint?: string; /** - * Whether to run browser e2e tests in headless mode. Defaults to true. + * Whether to run browser e2e tests in headless mode. + * + * Starting with Chrome v112, a new headless mode was introduced. + * The new headless mode unifies the "headful" and "headless" code paths in the Chrome distributable. + * + * To enable the "new" headless mode, a string value of "new" must be provided. + * To use the "old" headless mode, a boolean value of `true` must be provided. + * To use "headful" mode, a boolean value of `false` must be provided. + * + * Defaults to true. */ - browserHeadless?: boolean; + browserHeadless?: boolean | 'new'; /** * Slows down e2e browser operations by the specified amount of milliseconds. diff --git a/test/bundler/karma.config.ts b/test/bundler/karma.config.ts index db91d369272..1053a7cdacc 100644 --- a/test/bundler/karma.config.ts +++ b/test/bundler/karma.config.ts @@ -11,7 +11,7 @@ const localLaunchers = { base: CHROME_HEADLESS, flags: [ // run in headless mode (https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) - '--headless', + '--headless=new', // use --disable-gpu to avoid an error from a missing Mesa library (https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md) '--disable-gpu', // without a remote debugging port, Chrome exits immediately. diff --git a/test/karma/karma.config.js b/test/karma/karma.config.js index 926025d738e..8765ec09111 100644 --- a/test/karma/karma.config.js +++ b/test/karma/karma.config.js @@ -48,7 +48,7 @@ const localLaunchers = { flags: [ '--no-sandbox', // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md - '--headless', + '--headless=new', '--disable-gpu', // Without a remote debugging port, Google Chrome exits immediately. '--remote-debugging-port=9333',