Skip to content

Commit eea99ae

Browse files
fix: Fix issue with undefined or extra args passed to privileged commands (#27157)
1 parent 0107efb commit eea99ae

File tree

11 files changed

+74
-25
lines changed

11 files changed

+74
-25
lines changed

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ _Released 07/05/2023 (PENDING)_
99

1010
**Bugfixes:**
1111

12+
- Fixed issues where commands would fail with the error `must only be invoked from the spec file or support file`. Fixes [#27149](https://github.com/cypress-io/cypress/issues/27149).
1213
- Fixed an issue where chrome was not recovering from browser crashes properly. Fixes [#24650](https://github.com/cypress-io/cypress/issues/24650).
1314
- Fixed a race condition that was causing a GraphQL error to appear on the [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) when viewing a running Cypress Cloud build. Fixed in [#27134](https://github.com/cypress-io/cypress/pull/27134).
1415

packages/driver/cypress/e2e/commands/task.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe('src/cy/commands/task', () => {
216216
expect(lastLog.get('error')).to.eq(err)
217217
expect(lastLog.get('state')).to.eq('failed')
218218

219-
expect(err.message).to.eq(`\`cy.task('bar')\` failed with the following error:\n\nThe task 'bar' was not handled in the setupNodeEvents method. The following tasks are registered: return:arg, cypress:env, arg:is:undefined, wait, create:long:file, check:screenshot:size\n\nFix this in your setupNodeEvents method here:\n${Cypress.config('configFile')}`)
219+
expect(err.message).to.eq(`\`cy.task('bar')\` failed with the following error:\n\nThe task 'bar' was not handled in the setupNodeEvents method. The following tasks are registered: return:arg, return:foo, return:bar, return:baz, cypress:env, arg:is:undefined, wait, create:long:file, check:screenshot:size\n\nFix this in your setupNodeEvents method here:\n${Cypress.config('configFile')}`)
220220

221221
done()
222222
})

packages/driver/cypress/e2e/e2e/privileged_commands.cy.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,36 @@ describe('privileged commands', () => {
7070
})
7171

7272
it('handles undefined argument(s)', () => {
73-
cy.task('arg:is:undefined')
74-
cy.task('arg:is:undefined', undefined)
75-
cy.task('arg:is:undefined', undefined, undefined)
76-
cy.task('arg:is:undefined', undefined, { timeout: 9999 })
73+
// these intentionally use different tasks because otherwise there can
74+
// be false positives due to them equating to the same call
75+
cy.task('arg:is:undefined').should('equal', 'arg was undefined')
76+
cy.task('return:foo', undefined).should('equal', 'foo')
77+
cy.task('return:bar', undefined, undefined).should('equal', 'bar')
78+
cy.task('return:baz', undefined, { timeout: 9999 }).should('equal', 'baz')
7779
})
7880

7981
it('handles null argument(s)', () => {
80-
cy.task('return:arg', null)
81-
// @ts-ignore
82-
cy.task('return:arg', null, null)
83-
cy.task('return:arg', null, { timeout: 9999 })
82+
cy.task('return:arg', null).should('be.null')
83+
// @ts-expect-error
84+
cy.task('return:arg', null, null).should('be.null')
85+
cy.task('return:arg', null, { timeout: 9999 }).should('be.null')
86+
})
87+
88+
it('handles extra, unexpected arguments', () => {
89+
// @ts-expect-error
90+
cy.exec('echo "hey-o"', { log: true }, { should: 'be ignored' })
91+
// @ts-expect-error
92+
cy.readFile('cypress/fixtures/app.json', 'utf-8', { log: true }, { should: 'be ignored' })
93+
// @ts-expect-error
94+
cy.writeFile('cypress/_test-output/written.json', 'contents', 'utf-8', { log: true }, { should: 'be ignored' })
95+
// @ts-expect-error
96+
cy.task('return:arg', 'arg2', { log: true }, { should: 'be ignored' })
97+
// @ts-expect-error
98+
cy.get('#basic').selectFile('cypress/fixtures/valid.json', { log: true }, { should: 'be ignored' })
99+
if (!isWebkit) {
100+
// @ts-expect-error
101+
cy.origin('http://foobar.com:3500', {}, () => {}, { should: 'be ignored' })
102+
}
84103
})
85104

86105
it('passes in test body .then() callback', () => {

packages/driver/cypress/plugins/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ module.exports = async (on, config) => {
4747
'return:arg' (arg) {
4848
return arg
4949
},
50+
'return:foo' () {
51+
return 'foo'
52+
},
53+
'return:bar' () {
54+
return 'bar'
55+
},
56+
'return:baz' () {
57+
return 'baz'
58+
},
5059
'cypress:env' () {
5160
return process.env['CYPRESS']
5261
},

packages/driver/src/cy/commands/actions/selectFile.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,11 @@ export default (Commands, Cypress, cy, state, config) => {
283283
}
284284

285285
Commands.addAll({ prevSubject: 'element' }, {
286-
async selectFile (subject: JQuery<any>, files: Cypress.FileReference | Cypress.FileReference[], options: Partial<InternalSelectFileOptions>): Promise<JQuery> {
287-
const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined])
286+
async selectFile (subject: JQuery<any>, files: Cypress.FileReference | Cypress.FileReference[], options: Partial<InternalSelectFileOptions>, ...extras: never[]): Promise<JQuery> {
287+
// privileged commands need to send any and all args, even if not part
288+
// of their API, so they can be compared to the args collected when the
289+
// command is invoked
290+
const userArgs = trimUserArgs([files, _.isObject(options) ? { ...options } : undefined, ...extras])
288291

289292
options = _.defaults({}, options, {
290293
action: 'select',

packages/driver/src/cy/commands/exec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ interface InternalExecOptions extends Partial<Cypress.ExecOptions> {
1313

1414
export default (Commands, Cypress, cy) => {
1515
Commands.addAll({
16-
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions>) {
17-
const userArgs = trimUserArgs([cmd, userOptions])
16+
exec (cmd: string, userOptions: Partial<Cypress.ExecOptions>, ...extras: never[]) {
17+
// privileged commands need to send any and all args, even if not part
18+
// of their API, so they can be compared to the args collected when the
19+
// command is invoked
20+
const userArgs = trimUserArgs([cmd, userOptions, ...extras])
1821

1922
userOptions = userOptions || {}
2023

packages/driver/src/cy/commands/files.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ type WriteFileOptions = Partial<Cypress.WriteFileOptions & Cypress.Timeoutable>
2121

2222
export default (Commands, Cypress, cy, state) => {
2323
Commands.addAll({
24-
readFile (file: string, encoding: Cypress.Encodings | ReadFileOptions | undefined, userOptions?: ReadFileOptions) {
25-
const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
24+
readFile (file: string, encoding: Cypress.Encodings | ReadFileOptions | undefined, userOptions?: ReadFileOptions, ...extras: never[]) {
25+
// privileged commands need to send any and all args, even if not part
26+
// of their API, so they can be compared to the args collected when the
27+
// command is invoked
28+
const userArgs = trimUserArgs([file, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
2629

2730
if (_.isObject(encoding)) {
2831
userOptions = encoding
@@ -142,8 +145,11 @@ export default (Commands, Cypress, cy, state) => {
142145
return verifyAssertions()
143146
},
144147

145-
writeFile (fileName: string, contents: string, encoding: Cypress.Encodings | WriteFileOptions | undefined, userOptions: WriteFileOptions) {
146-
const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined])
148+
writeFile (fileName: string, contents: string, encoding: Cypress.Encodings | WriteFileOptions | undefined, userOptions: WriteFileOptions, ...extras: never[]) {
149+
// privileged commands need to send any and all args, even if not part
150+
// of their API, so they can be compared to the args collected when the
151+
// command is invoked
152+
const userArgs = trimUserArgs([fileName, contents, encoding, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
147153

148154
if (_.isObject(encoding)) {
149155
userOptions = encoding

packages/driver/src/cy/commands/origin/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,28 @@ function stringifyFn (fn?: any) {
3131
return _.isFunction(fn) ? fn.toString() : undefined
3232
}
3333

34-
function getUserArgs<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
34+
function getUserArgs<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, extras: never[], fn?: Fn<T>) {
3535
return trimUserArgs([
3636
urlOrDomain,
3737
fn && _.isObject(optionsOrFn) ? { ...optionsOrFn } : stringifyFn(optionsOrFn),
3838
fn ? stringifyFn(fn) : undefined,
39+
...extras,
3940
])
4041
}
4142

4243
export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: StateFunc, config: Cypress.InternalConfig) => {
4344
const communicator = Cypress.primaryOriginCommunicator
4445

4546
Commands.addAll({
46-
origin<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>) {
47+
origin<T> (urlOrDomain: string, optionsOrFn: OptionsOrFn<T>, fn?: Fn<T>, ...extras: never[]) {
4748
if (Cypress.isBrowser('webkit')) {
4849
return $errUtils.throwErrByPath('webkit.origin')
4950
}
5051

51-
const userArgs = getUserArgs<T>(urlOrDomain, optionsOrFn, fn)
52+
// privileged commands need to send any and all args, even if not part
53+
// of their API, so they can be compared to the args collected when the
54+
// command is invoked
55+
const userArgs = getUserArgs<T>(urlOrDomain, optionsOrFn, extras, fn)
5256

5357
const userInvocationStack = state('current').get('userInvocationStack')
5458

packages/driver/src/cy/commands/task.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ interface InternalTaskOptions extends Partial<Cypress.Loggable & Cypress.Timeout
1414

1515
export default (Commands, Cypress, cy) => {
1616
Commands.addAll({
17-
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable>) {
18-
const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined])
17+
task (task, arg, userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable>, ...extras: never[]) {
18+
// privileged commands need to send any and all args, even if not part
19+
// of their API, so they can be compared to the args collected when the
20+
// command is invoked
21+
const userArgs = trimUserArgs([task, arg, _.isObject(userOptions) ? { ...userOptions } : undefined, ...extras])
1922

2023
userOptions = userOptions || {}
2124

packages/driver/src/util/privileged_channel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function runPrivilegedCommand ({ commandName, cy, Cypress, options, userA
3535
})
3636
}
3737

38+
// removes trailing undefined args
3839
export function trimUserArgs (args: any[]) {
3940
return _.dropRightWhile(args, _.isUndefined)
4041
}

0 commit comments

Comments
 (0)