From b6c7a775e6d383d6af42eef636ef559173cdaacf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 01:16:49 +0000 Subject: [PATCH 1/4] fix(config): disallow setting viewportHeight/viewportWidth via Cypress.config() during test execution `viewportWidth` and `viewportHeight` could be mutated at run-time via `Cypress.config()`. Doing so affected the *next* test rather than the current one and was not reflected in Test Replay, since these run-time config updates are not shared with the cloud. Introduce a new `suiteOrTest` override level that permits suite- and test-level config overrides (describe/it) but rejects run-time mutation via `Cypress.config()`, and apply it to `viewportWidth`/`viewportHeight`. Users should use `cy.viewport()` or set the viewport in a `describe`/`it` config override instead. Cross-origin config syncing applies its diff via `Cypress.config()` at run-time, so `suiteOrTest` values are now omitted from the sync (viewport is already propagated to spec bridges via state). Closes #31592 --- cli/CHANGELOG.md | 1 + packages/config/src/browser.ts | 13 +++- packages/config/src/options.ts | 22 ++++++- packages/config/test/index.spec.ts | 61 ++++++++++++++++--- .../cypress/e2e/e2e/testConfigOverrides.cy.js | 22 +++++++ packages/driver/cypress/e2e/util/config.cy.js | 36 +++++++++++ packages/driver/src/cypress/error_messages.ts | 4 ++ packages/driver/src/util/config.ts | 22 +++++-- 8 files changed, 162 insertions(+), 19 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index fd69ae845f6..8e7b22f42f9 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -11,6 +11,7 @@ - The default `keystrokeDelay` for `cy.type()` has been changed from `10` to `0` to improve performance of typing-heavy test runs. Users who relied on the implicit 10ms delay can restore it by setting `keystrokeDelay: 10` in their Cypress config or on `Cypress.Keyboard`. Addresses [#33523](https://github.com/cypress-io/cypress/issues/33523). - Rewrote the experimental `experimentalFastVisibility` algorithm to delegate to the browser's native `Element.checkVisibility()` with a zero-dimension guard. Addressed in [#33781](https://github.com/cypress-io/cypress/pull/33781). - The `experimentalFastVisibility` boolean flag has been replaced with a new `visibilityStrategy` config option that accepts `'legacy'` or `'modern'`, defaulting to `'modern'`. The modern visibility algorithm (based on `Element.checkVisibility()`) is now the default for all users. The option is deprecated — set `visibilityStrategy: 'legacy'` only if you need the old algorithm when migrating to the modern visibility algorithm. Addressed in [#33794](https://github.com/cypress-io/cypress/pull/33794). +- `viewportWidth` and `viewportHeight` can no longer be set via [`Cypress.config()`](https://on.cypress.io/config) during test execution. Mutating them this way affected the *next* test rather than the current one and was not reflected in Test Replay. Use [`cy.viewport()`](https://on.cypress.io/viewport) or set them in the test configuration of a `describe`/`context` or `it` block instead. Addresses [#31592](https://github.com/cypress-io/cypress/issues/31592). - [`cy.getCookie()`](https://on.cypress.io/getcookie) and [`cy.getCookies()`](https://on.cypress.io/getcookies) are now query commands. They re-read the cookie(s) from the browser and retry any attached assertions until they pass or the command times out, so `cy.getCookie('token').should('exist')` now waits for a cookie that is set asynchronously (for example, after a login request resolves) instead of failing on the first read. As a result, their timeout now follows `defaultCommandTimeout` (default `4000`) rather than `responseTimeout` (default `30000`), and to overwrite these commands you must now use [`Cypress.Commands.overwriteQuery()`](https://on.cypress.io/api/cypress-api/custom-queries) instead of `Cypress.Commands.overwrite()`. Addresses [#4802](https://github.com/cypress-io/cypress/issues/4802). **Dependency Updates:** diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index c1dfe3d95ab..822a47236e6 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -10,7 +10,7 @@ import { } from './options' import type { BreakingErrResult, TestingType } from '@packages/types' -import type { BreakingOption, BreakingOptionErrorKey, OverrideLevel } from './options' +import type { BreakingOption, BreakingOptionErrorKey, CurrentOverrideLevel, OverrideLevel } from './options' import type { ErrResult } from './validation' // this export has to be done in 2 lines because of a bug in babel typescript @@ -205,7 +205,10 @@ export const validateNoBreakingTestingTypeConfig = (cfg: any, testingType: keyof return validateNoBreakingOptions(options, cfg, onWarning, onErr, testingType) } -export const validateOverridableAtRunTime = (config: any, isSuiteLevelOverride: boolean, onErr: (result: InvalidTestOverrideResult) => void) => { +export const validateOverridableAtRunTime = (config: any, currentOverrideLevel: CurrentOverrideLevel, onErr: (result: InvalidTestOverrideResult) => void) => { + const isSuiteLevelOverride = currentOverrideLevel === 'suite' + const isSuiteOrTestLevelOverride = currentOverrideLevel === 'suite' || currentOverrideLevel === 'test' + Object.keys(config).some((configKey) => { const overrideLevel: OverrideLevel = testOverrideLevels[configKey] @@ -237,7 +240,11 @@ export const validateOverridableAtRunTime = (config: any, isSuiteLevelOverride: // TODO: add a hook to ensure valid testing-type configuration is being set at runtime for all configuration values. // https://github.com/cypress-io/cypress/issues/24365 - if (overrideLevel === 'never' || (overrideLevel === 'suite' && !isSuiteLevelOverride)) { + if ( + overrideLevel === 'never' || + (overrideLevel === 'suite' && !isSuiteLevelOverride) || + (overrideLevel === 'suiteOrTest' && !isSuiteOrTestLevelOverride) + ) { onErr({ invalidConfigKey: configKey, supportedOverrideLevel: overrideLevel, diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index f7346b36009..01bc190db95 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -40,7 +40,23 @@ type ValidationOptions = { export type BreakingOptionErrorKey = typeof BREAKING_OPTION_ERROR_KEY[number] -export type OverrideLevel = 'any' | 'suite' | 'never' +/** + * Where a configuration option is allowed to be overridden at test time: + * - `any`: suite-level overrides, test-level overrides, and at run-time via `Cypress.config()`. + * - `suiteOrTest`: suite-level and test-level overrides only. Cannot be set at run-time via + * `Cypress.config()` during test execution. + * - `suite`: suite-level overrides only. + * - `never`: cannot be overridden at test time. + */ +export type OverrideLevel = 'any' | 'suiteOrTest' | 'suite' | 'never' + +/** + * The current level at which a test-time override is being applied. `suite` and `test` + * correspond to `describe`/`context` and `it`/`test` config overrides respectively, while + * `undefined` indicates a run-time mutation via `Cypress.config()` during test execution + * (or while loading the support/spec file or in a `test:before:run` event). + */ +export type CurrentOverrideLevel = 'suite' | 'test' | undefined interface ConfigOption { name: string @@ -477,12 +493,12 @@ const driverConfigOptions: Array = [ name: 'viewportHeight', defaultValue: (options: Record = {}) => options.testingType === 'component' ? 500 : 660, validation: validate.isNumber, - overrideLevel: 'any', + overrideLevel: 'suiteOrTest', }, { name: 'viewportWidth', defaultValue: (options: Record = {}) => options.testingType === 'component' ? 500 : 1000, validation: validate.isNumber, - overrideLevel: 'any', + overrideLevel: 'suiteOrTest', }, { name: 'waitForAnimations', defaultValue: true, diff --git a/packages/config/test/index.spec.ts b/packages/config/test/index.spec.ts index 3c32afe69f3..57aeb9252e9 100644 --- a/packages/config/test/index.spec.ts +++ b/packages/config/test/index.spec.ts @@ -225,7 +225,7 @@ describe('config/src/index', () => { it('calls onError handler if configuration override level=never', () => { const errorFn = vi.fn() - configUtil.validateOverridableAtRunTime({ chromeWebSecurity: false }, false, errorFn) + configUtil.validateOverridableAtRunTime({ chromeWebSecurity: false }, undefined, errorFn) expect(errorFn).toHaveBeenCalledTimes(1) expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ @@ -238,19 +238,27 @@ describe('config/src/index', () => { it('does not calls onError handler if validating level is suite', () => { const errorFn = vi.fn() - const isSuiteOverride = true - - configUtil.validateOverridableAtRunTime({ testIsolation: true }, isSuiteOverride, errorFn) + configUtil.validateOverridableAtRunTime({ testIsolation: true }, 'suite', errorFn) expect(errorFn).toHaveBeenCalledTimes(0) }) - it('calls onError handler if validating level is not suite', () => { + it('calls onError handler if validating level is test', () => { const errorFn = vi.fn() - const isSuiteOverride = false + configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, 'test', errorFn) - configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, isSuiteOverride, errorFn) + expect(errorFn).toHaveBeenCalledTimes(1) + expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ + invalidConfigKey: 'testIsolation', + supportedOverrideLevel: 'suite', + })) + }) + + it('calls onError handler if validating level is run-time', () => { + const errorFn = vi.fn() + + configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, undefined, errorFn) expect(errorFn).toHaveBeenCalledTimes(1) expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ @@ -260,10 +268,45 @@ describe('config/src/index', () => { }) }) + describe('configuration override level=suiteOrTest', () => { + it('does not call onError handler if validating level is suite', () => { + const errorFn = vi.fn() + + configUtil.validateOverridableAtRunTime({ viewportWidth: 200 }, 'suite', errorFn) + + expect(errorFn).toHaveBeenCalledTimes(0) + }) + + it('does not call onError handler if validating level is test', () => { + const errorFn = vi.fn() + + configUtil.validateOverridableAtRunTime({ viewportHeight: 100 }, 'test', errorFn) + + expect(errorFn).toHaveBeenCalledTimes(0) + }) + + it('calls onError handler if validating level is run-time', () => { + const errorFn = vi.fn() + + configUtil.validateOverridableAtRunTime({ viewportWidth: 200, viewportHeight: 100 }, undefined, errorFn) + + expect(errorFn).toHaveBeenCalledTimes(2) + expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ + invalidConfigKey: 'viewportWidth', + supportedOverrideLevel: 'suiteOrTest', + })) + + expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ + invalidConfigKey: 'viewportHeight', + supportedOverrideLevel: 'suiteOrTest', + })) + }) + }) + it(`does not call onErr if config override level=any`, () => { const errorFn = vi.fn() - configUtil.validateOverridableAtRunTime({ requestTimeout: 1000 }, false, errorFn) + configUtil.validateOverridableAtRunTime({ requestTimeout: 1000 }, undefined, errorFn) expect(errorFn).toHaveBeenCalledTimes(0) }) @@ -271,7 +314,7 @@ describe('config/src/index', () => { it('does not call onErr if configuration is a non-Cypress config option', () => { const errorFn = vi.fn() - configUtil.validateOverridableAtRunTime({ foo: 'bar' }, true, errorFn) + configUtil.validateOverridableAtRunTime({ foo: 'bar' }, 'suite', errorFn) expect(errorFn).toHaveBeenCalledTimes(0) }) diff --git a/packages/driver/cypress/e2e/e2e/testConfigOverrides.cy.js b/packages/driver/cypress/e2e/e2e/testConfigOverrides.cy.js index 89673aa41d9..b1711caa236 100644 --- a/packages/driver/cypress/e2e/e2e/testConfigOverrides.cy.js +++ b/packages/driver/cypress/e2e/e2e/testConfigOverrides.cy.js @@ -372,6 +372,28 @@ describe('cannot set override configuration options that', () => { Cypress.config('chromeWebSecurity', false) }) + // https://github.com/cypress-io/cypress/issues/31592 + it('throws if mutating viewportWidth with Cypress.config() during test execution', (done) => { + window.top.__cySkipValidateConfig = false + cy.once('fail', (err) => { + expect(err.message).to.include('`Cypress.config()` cannot override `viewportWidth` during test execution') + done() + }) + + Cypress.config('viewportWidth', 200) + }) + + // https://github.com/cypress-io/cypress/issues/31592 + it('throws if mutating viewportHeight with Cypress.config() during test execution', (done) => { + window.top.__cySkipValidateConfig = false + cy.once('fail', (err) => { + expect(err.message).to.include('`Cypress.config()` cannot override `viewportHeight` during test execution') + done() + }) + + Cypress.config('viewportHeight', 100) + }) + it('does not throw for non-Cypress config values', () => { expect(() => { Cypress.config('foo', 'bar') diff --git a/packages/driver/cypress/e2e/util/config.cy.js b/packages/driver/cypress/e2e/util/config.cy.js index 0f76a2eb13a..4465c6793f5 100644 --- a/packages/driver/cypress/e2e/util/config.cy.js +++ b/packages/driver/cypress/e2e/util/config.cy.js @@ -132,6 +132,42 @@ describe('driver/src/cypress/validate_config', () => { }) }) + describe('when config override level is suiteOrTest', () => { + ['test', 'suite'].forEach((mocha_runnable) => { + it(`does not throw when runtime level is ${mocha_runnable}`, () => { + const state = $SetterGetter.create({ + duringUserTestExecution: false, + test: { + _testConfig: { applied: mocha_runnable }, + }, + specWindow: { Error }, + }) + const overrideLevel = getMochaOverrideLevel(state) + + expect(overrideLevel).to.eq(mocha_runnable) + + expect(() => { + validateConfig(state, { viewportWidth: 200, viewportHeight: 100 }) + }).not.to.throw() + }) + }) + + it('throws when mutated at run-time with Cypress.config()', () => { + const state = $SetterGetter.create({ + duringUserTestExecution: true, + specWindow: { Error }, + runnable: { type: 'test' }, + }) + const overrideLevel = getMochaOverrideLevel(state) + + expect(overrideLevel).to.be.undefined + + expect(() => { + validateConfig(state, { viewportWidth: 200 }) + }).to.throw(`\`Cypress.config()\` cannot override \`viewportWidth\` during test execution`) + }) + }) + describe('when config override level is suite', () => { it('and config override is read-only', () => { const state = $SetterGetter.create({ diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index bbbfc01dc9c..0888ec046b7 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -262,6 +262,10 @@ export default { message: `\`Cypress.config()\` can never override \`{{invalidConfigKey}}\` because it is a read-only configuration option.`, docsUrl: 'https://on.cypress.io/config', }, + suite_or_test_only: { + message: `\`Cypress.config()\` cannot override \`{{invalidConfigKey}}\` during test execution because it would affect the next test rather than the current one and is not reflected in Test Replay. Set \`{{invalidConfigKey}}\` in the test configuration of a \`describe\`/\`context\` or \`it\` block, or use \`cy.viewport()\` instead.`, + docsUrl: 'https://on.cypress.io/config', + }, }, invalid_mocha_config_override: { read_only: { diff --git a/packages/driver/src/util/config.ts b/packages/driver/src/util/config.ts index 4b5b3917970..b3afb18b4e1 100644 --- a/packages/driver/src/util/config.ts +++ b/packages/driver/src/util/config.ts @@ -25,7 +25,14 @@ const omitConfigReadOnlyDifferences = (objectLikeConfig: Cypress.ObjectLike) => return } - if ((overrideLevels === 'never' && configKey !== 'isDefaultProtocolEnabled')) { + // Cross-origin syncing applies the diff via Cypress.config() at run-time, so omit any + // value that cannot be set that way. `never` values are read-only, and `suiteOrTest` + // values (e.g. viewportWidth/viewportHeight) can only be set via suite-/test-level + // overrides. Viewport is synced to spec bridges via state, not config, so it is safe to omit. + if ( + (overrideLevels === 'never' && configKey !== 'isDefaultProtocolEnabled') || + overrideLevels === 'suiteOrTest' + ) { delete objectLikeConfig[configKey] } }) @@ -115,15 +122,22 @@ export const validateConfig = (state: State, config: Record, skipCo const mochaOverrideLevel = getMochaOverrideLevel(state) if (!skipConfigOverrideValidation && mochaOverrideLevel !== 'restoring') { - const isSuiteOverride = mochaOverrideLevel === 'suite' - - validateOverridableAtRunTime(config, isSuiteOverride, (validationResult) => { + // `mochaOverrideLevel` is 'suite'/'test' when applying suite-/test-level config + // overrides (describe/it). Any other value (e.g. undefined) means the config is being + // mutated at run-time via Cypress.config() during test execution. + const currentOverrideLevel = mochaOverrideLevel === 'suite' || mochaOverrideLevel === 'test' + ? mochaOverrideLevel + : undefined + + validateOverridableAtRunTime(config, currentOverrideLevel, (validationResult) => { let errKey = 'config.cypress_config_api.read_only' if (validationResult.supportedOverrideLevel === 'global_only') { errKey = 'config.invalid_mocha_config_override.global_only' } else if (validationResult.supportedOverrideLevel === 'suite') { errKey = 'config.invalid_mocha_config_override.suite_only' + } else if (validationResult.supportedOverrideLevel === 'suiteOrTest') { + errKey = 'config.cypress_config_api.suite_or_test_only' } else if (mochaOverrideLevel) { errKey = 'config.invalid_mocha_config_override.read_only' } From 1da72722ad5ffeb1b31c3c72366080dd637d92e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 20:20:54 +0000 Subject: [PATCH 2/4] test(driver): use defaultCommandTimeout for invalid-value config assertion viewportHeight/viewportWidth are now rejected at run-time by the override-level check before value validation runs, so asserting an invalid viewport value at run-time no longer reaches the value-type error. Use defaultCommandTimeout (overridable at any level) to keep exercising the value-validation path. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RhjKrLZmvqXKhgKsDRtXAB --- packages/driver/cypress/e2e/util/config.cy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/cypress/e2e/util/config.cy.js b/packages/driver/cypress/e2e/util/config.cy.js index 4465c6793f5..6225cdd86f0 100644 --- a/packages/driver/cypress/e2e/util/config.cy.js +++ b/packages/driver/cypress/e2e/util/config.cy.js @@ -240,8 +240,8 @@ describe('driver/src/cypress/validate_config', () => { }) expect(() => { - validateConfig(state, { viewportHeight: '300' }) - }).to.throw(`Expected \`viewportHeight\` to be a number.\n\nInstead the value was: \`"300"\``) + validateConfig(state, { defaultCommandTimeout: '300' }) + }).to.throw(`Expected \`defaultCommandTimeout\` to be a number.\n\nInstead the value was: \`"300"\``) }) }) }) From 022ca02b35bb5e6ac8c24466b0b9086809de8195 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 20:28:35 +0000 Subject: [PATCH 3/4] refactor(config): make suiteOrTest override error generic with per-key guidance Rework the `suite_or_test_only` message so the base text is config-key agnostic and option-specific advice is appended via an `additionalInfo` suffix, keyed by config option in the driver. viewportWidth/viewportHeight append the `cy.viewport()` recommendation; future suiteOrTest options can add their own guidance without changing the shared message. Also trims verbose inline comments per review feedback. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RhjKrLZmvqXKhgKsDRtXAB --- packages/config/src/options.ts | 1 - packages/driver/src/cypress/error_messages.ts | 2 +- packages/driver/src/util/config.ts | 19 ++++++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index a91990389c6..ae928ab2d92 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -53,7 +53,6 @@ export type OverrideLevel = 'any' | 'suiteOrTest' | 'suite' | 'never' /** * The current level at which a test-time override is being applied. `suite` and `test` - * correspond to `describe`/`context` and `it`/`test` config overrides respectively, while * `undefined` indicates a run-time mutation via `Cypress.config()` during test execution * (or while loading the support/spec file or in a `test:before:run` event). */ diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 0888ec046b7..751d1e8d3a4 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -263,7 +263,7 @@ export default { docsUrl: 'https://on.cypress.io/config', }, suite_or_test_only: { - message: `\`Cypress.config()\` cannot override \`{{invalidConfigKey}}\` during test execution because it would affect the next test rather than the current one and is not reflected in Test Replay. Set \`{{invalidConfigKey}}\` in the test configuration of a \`describe\`/\`context\` or \`it\` block, or use \`cy.viewport()\` instead.`, + message: `\`Cypress.config()\` cannot override \`{{invalidConfigKey}}\` during test execution because it would affect the next test rather than the current one and is not reflected in Test Replay. Set \`{{invalidConfigKey}}\` in the test configuration of a \`describe\`/\`context\` or \`it\` block instead.{{additionalInfo}}`, docsUrl: 'https://on.cypress.io/config', }, }, diff --git a/packages/driver/src/util/config.ts b/packages/driver/src/util/config.ts index b3afb18b4e1..2017502b6e5 100644 --- a/packages/driver/src/util/config.ts +++ b/packages/driver/src/util/config.ts @@ -26,9 +26,7 @@ const omitConfigReadOnlyDifferences = (objectLikeConfig: Cypress.ObjectLike) => } // Cross-origin syncing applies the diff via Cypress.config() at run-time, so omit any - // value that cannot be set that way. `never` values are read-only, and `suiteOrTest` - // values (e.g. viewportWidth/viewportHeight) can only be set via suite-/test-level - // overrides. Viewport is synced to spec bridges via state, not config, so it is safe to omit. + // value that cannot be set that way. if ( (overrideLevels === 'never' && configKey !== 'isDefaultProtocolEnabled') || overrideLevels === 'suiteOrTest' @@ -107,6 +105,13 @@ export const getMochaOverrideLevel = (state): MochaOverrideLevel | undefined => return undefined } +// Extra guidance appended to the generic `suite_or_test_only` error, keyed by config option. +// Add an entry here when a new `suiteOrTest` option needs option-specific advice. +const suiteOrTestOnlyGuidance: Record = { + viewportWidth: ' To change the viewport during a test, use `cy.viewport()`.', + viewportHeight: ' To change the viewport during a test, use `cy.viewport()`.', +} + // Configuration can be override at multiple run-time levels. Ensure the configuration keys can // be override and that the provided override values are the correct type. // @@ -122,9 +127,6 @@ export const validateConfig = (state: State, config: Record, skipCo const mochaOverrideLevel = getMochaOverrideLevel(state) if (!skipConfigOverrideValidation && mochaOverrideLevel !== 'restoring') { - // `mochaOverrideLevel` is 'suite'/'test' when applying suite-/test-level config - // overrides (describe/it). Any other value (e.g. undefined) means the config is being - // mutated at run-time via Cypress.config() during test execution. const currentOverrideLevel = mochaOverrideLevel === 'suite' || mochaOverrideLevel === 'test' ? mochaOverrideLevel : undefined @@ -142,7 +144,10 @@ export const validateConfig = (state: State, config: Record, skipCo errKey = 'config.invalid_mocha_config_override.read_only' } - throw new (state('specWindow').Error)($errUtils.errByPath(errKey, validationResult)) + throw new (state('specWindow').Error)($errUtils.errByPath(errKey, { + ...validationResult, + additionalInfo: suiteOrTestOnlyGuidance[validationResult.invalidConfigKey] ?? '', + })) }) } From 859fe05e38964ad2b265057f13918db1e261c589 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 14:43:11 +0000 Subject: [PATCH 4/4] fix(config): only block viewport via Cypress.config() during test execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suiteOrTest override check previously rejected viewportWidth/viewportHeight whenever the override context was not a suite/test config override, which also covered support/spec file load and test:before:run events — broader than the intended "during test execution" scope. Introduce a distinct `runtime` context (state.duringUserTestExecution) and only reject suiteOrTest options then, so spec-wide viewport defaults set via Cypress.config() at file load work again. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RhjKrLZmvqXKhgKsDRtXAB --- packages/config/src/browser.ts | 7 ++----- packages/config/src/options.ts | 12 ++++++------ packages/config/test/index.spec.ts | 12 ++++++++++-- packages/driver/cypress/e2e/util/config.cy.js | 15 +++++++++++++++ packages/driver/src/util/config.ts | 2 +- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index 822a47236e6..f58b1d781b7 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -206,9 +206,6 @@ export const validateNoBreakingTestingTypeConfig = (cfg: any, testingType: keyof } export const validateOverridableAtRunTime = (config: any, currentOverrideLevel: CurrentOverrideLevel, onErr: (result: InvalidTestOverrideResult) => void) => { - const isSuiteLevelOverride = currentOverrideLevel === 'suite' - const isSuiteOrTestLevelOverride = currentOverrideLevel === 'suite' || currentOverrideLevel === 'test' - Object.keys(config).some((configKey) => { const overrideLevel: OverrideLevel = testOverrideLevels[configKey] @@ -242,8 +239,8 @@ export const validateOverridableAtRunTime = (config: any, currentOverrideLevel: if ( overrideLevel === 'never' || - (overrideLevel === 'suite' && !isSuiteLevelOverride) || - (overrideLevel === 'suiteOrTest' && !isSuiteOrTestLevelOverride) + (overrideLevel === 'suite' && currentOverrideLevel !== 'suite') || + (overrideLevel === 'suiteOrTest' && currentOverrideLevel === 'runtime') ) { onErr({ invalidConfigKey: configKey, diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index ae928ab2d92..1a83965de79 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -44,19 +44,19 @@ export type BreakingOptionErrorKey = typeof BREAKING_OPTION_ERROR_KEY[number] /** * Where a configuration option is allowed to be overridden at test time: * - `any`: suite-level overrides, test-level overrides, and at run-time via `Cypress.config()`. - * - `suiteOrTest`: suite-level and test-level overrides only. Cannot be set at run-time via - * `Cypress.config()` during test execution. + * - `suiteOrTest`: suite-level and test-level overrides only. Cannot be set via `Cypress.config()` + * while a test is executing. * - `suite`: suite-level overrides only. * - `never`: cannot be overridden at test time. */ export type OverrideLevel = 'any' | 'suiteOrTest' | 'suite' | 'never' /** - * The current level at which a test-time override is being applied. `suite` and `test` - * `undefined` indicates a run-time mutation via `Cypress.config()` during test execution - * (or while loading the support/spec file or in a `test:before:run` event). + * The context in which a test-time config change is being applied: `suite`/`test` for + * `describe`/`it` config overrides, `runtime` for a `Cypress.config()` call while a test is + * executing, and `undefined` for anything else (support/spec file load or a `test:before:run` event). */ -export type CurrentOverrideLevel = 'suite' | 'test' | undefined +export type CurrentOverrideLevel = 'suite' | 'test' | 'runtime' | undefined interface ConfigOption { name: string diff --git a/packages/config/test/index.spec.ts b/packages/config/test/index.spec.ts index d32c087d9e7..637bf1d706c 100644 --- a/packages/config/test/index.spec.ts +++ b/packages/config/test/index.spec.ts @@ -278,7 +278,7 @@ describe('config/src/index', () => { it('calls onError handler if validating level is run-time', () => { const errorFn = vi.fn() - configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, undefined, errorFn) + configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, 'runtime', errorFn) expect(errorFn).toHaveBeenCalledTimes(1) expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ @@ -305,11 +305,19 @@ describe('config/src/index', () => { expect(errorFn).toHaveBeenCalledTimes(0) }) - it('calls onError handler if validating level is run-time', () => { + it('does not call onError handler outside test execution (e.g. file load)', () => { const errorFn = vi.fn() configUtil.validateOverridableAtRunTime({ viewportWidth: 200, viewportHeight: 100 }, undefined, errorFn) + expect(errorFn).toHaveBeenCalledTimes(0) + }) + + it('calls onError handler if validating level is run-time', () => { + const errorFn = vi.fn() + + configUtil.validateOverridableAtRunTime({ viewportWidth: 200, viewportHeight: 100 }, 'runtime', errorFn) + expect(errorFn).toHaveBeenCalledTimes(2) expect(errorFn).toHaveBeenCalledWith(expect.objectContaining({ invalidConfigKey: 'viewportWidth', diff --git a/packages/driver/cypress/e2e/util/config.cy.js b/packages/driver/cypress/e2e/util/config.cy.js index 6225cdd86f0..d0a8ad2bc24 100644 --- a/packages/driver/cypress/e2e/util/config.cy.js +++ b/packages/driver/cypress/e2e/util/config.cy.js @@ -166,6 +166,21 @@ describe('driver/src/cypress/validate_config', () => { validateConfig(state, { viewportWidth: 200 }) }).to.throw(`\`Cypress.config()\` cannot override \`viewportWidth\` during test execution`) }) + + it('does not throw when set outside test execution (e.g. support/spec file load)', () => { + const state = $SetterGetter.create({ + duringUserTestExecution: false, + test: undefined, + specWindow: { Error }, + }) + const overrideLevel = getMochaOverrideLevel(state) + + expect(overrideLevel).to.be.undefined + + expect(() => { + validateConfig(state, { viewportWidth: 200, viewportHeight: 100 }) + }).not.to.throw() + }) }) describe('when config override level is suite', () => { diff --git a/packages/driver/src/util/config.ts b/packages/driver/src/util/config.ts index 2017502b6e5..822693e1472 100644 --- a/packages/driver/src/util/config.ts +++ b/packages/driver/src/util/config.ts @@ -129,7 +129,7 @@ export const validateConfig = (state: State, config: Record, skipCo if (!skipConfigOverrideValidation && mochaOverrideLevel !== 'restoring') { const currentOverrideLevel = mochaOverrideLevel === 'suite' || mochaOverrideLevel === 'test' ? mochaOverrideLevel - : undefined + : state('duringUserTestExecution') ? 'runtime' : undefined validateOverridableAtRunTime(config, currentOverrideLevel, (validationResult) => { let errKey = 'config.cypress_config_api.read_only'