Skip to content

Commit 32d5902

Browse files
fix: Add more precise types to Cypress.Commands (#19003)
Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
1 parent 4a97a52 commit 32d5902

File tree

2 files changed

+136
-12
lines changed

2 files changed

+136
-12
lines changed

cli/types/cypress.d.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,38 @@ declare namespace Cypress {
77
type HttpMethod = string
88
type RequestBody = string | object
99
type ViewportOrientation = 'portrait' | 'landscape'
10-
type PrevSubject = 'optional' | 'element' | 'document' | 'window'
10+
type PrevSubject = keyof PrevSubjectMap
1111
type TestingType = 'e2e' | 'component'
1212
type PluginConfig = (on: PluginEvents, config: PluginConfigOptions) => void | ConfigOptions | Promise<ConfigOptions>
1313

14+
interface PrevSubjectMap<O = unknown> {
15+
optional: O
16+
element: JQuery
17+
document: Document
18+
window: Window
19+
}
20+
1421
interface CommandOptions {
1522
prevSubject: boolean | PrevSubject | PrevSubject[]
1623
}
24+
interface CommandFn<T extends keyof ChainableMethods> {
25+
(this: Mocha.Context, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
26+
}
27+
interface CommandFnWithSubject<T extends keyof ChainableMethods, S> {
28+
(this: Mocha.Context, prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
29+
}
30+
interface CommandOriginalFn<T extends keyof ChainableMethods> extends CallableFunction {
31+
(...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]>
32+
}
33+
interface CommandOriginalFnWithSubject<T extends keyof ChainableMethods, S> extends CallableFunction {
34+
(prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]>
35+
}
36+
interface CommandFnWithOriginalFn<T extends keyof Chainable> {
37+
(this: Mocha.Context, originalFn: CommandOriginalFn<T>, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
38+
}
39+
interface CommandFnWithOriginalFnAndSubject<T extends keyof Chainable, S> {
40+
(this: Mocha.Context, originalFn: CommandOriginalFnWithSubject<T, S>, prevSubject: S, ...args: Parameters<ChainableMethods[T]>): ReturnType<ChainableMethods[T]> | void
41+
}
1742
interface ObjectLike {
1843
[key: string]: any
1944
}
@@ -420,9 +445,16 @@ declare namespace Cypress {
420445
* @see https://on.cypress.io/api/commands
421446
*/
422447
Commands: {
423-
add<T extends keyof Chainable>(name: T, fn: Chainable[T]): void
424-
add<T extends keyof Chainable>(name: T, options: CommandOptions, fn: Chainable[T]): void
425-
overwrite<T extends keyof Chainable>(name: T, fn: Chainable[T]): void
448+
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
449+
add<T extends keyof Chainable>(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn<T>): void
450+
add<T extends keyof Chainable, S extends PrevSubject>(
451+
name: T, options: CommandOptions & { prevSubject: true | S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
452+
): void
453+
add<T extends keyof Chainable, S extends PrevSubject>(
454+
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
455+
): void
456+
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
457+
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
426458
}
427459

428460
/**
@@ -2248,6 +2280,12 @@ declare namespace Cypress {
22482280
$$<TElement extends Element = HTMLElement>(selector: JQuery.Selector, context?: Element | Document | JQuery): JQuery<TElement>
22492281
}
22502282

2283+
type ChainableMethods<Subject = any> = {
2284+
[P in keyof Chainable<Subject>]: Chainable<Subject>[P] extends ((...args: any[]) => any)
2285+
? Chainable<Subject>[P]
2286+
: never
2287+
}
2288+
22512289
interface SinonSpyAgent<A extends sinon.SinonSpy> {
22522290
log(shouldOutput?: boolean): Omit<A, 'withArgs'> & Agent<A>
22532291

cli/types/tests/cypress-tests.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ namespace CypressIsCyTests {
6464

6565
declare namespace Cypress {
6666
interface Chainable {
67-
newCommand: (arg: string) => void
67+
newCommand: (arg: string) => Chainable<number>
6868
}
6969
}
7070

@@ -74,20 +74,106 @@ namespace CypressCommandsTests {
7474
arg
7575
return
7676
})
77-
Cypress.Commands.add('newCommand', { prevSubject: true }, (arg) => {
77+
Cypress.Commands.add('newCommand', (arg) => {
7878
// $ExpectType string
7979
arg
80+
})
81+
Cypress.Commands.add('newCommand', function(arg) {
82+
this // $ExpectType Context
83+
arg // $ExpectType string
84+
})
85+
Cypress.Commands.add('newCommand', { prevSubject: true }, (subject, arg) => {
86+
subject // $ExpectType unknown
87+
arg // $ExpectType string
8088
return
8189
})
82-
Cypress.Commands.add('newCommand', (arg) => {
83-
// $ExpectType string
84-
arg
85-
return new Promise((resolve) => {})
90+
Cypress.Commands.add('newCommand', { prevSubject: false }, (arg) => {
91+
arg // $ExpectType string
92+
return
93+
})
94+
Cypress.Commands.add('newCommand', { prevSubject: 'optional' }, (subject, arg) => {
95+
subject // $ExpectType unknown
96+
arg // $ExpectType string
97+
return
8698
})
87-
Cypress.Commands.overwrite('newCommand', (arg) => {
99+
Cypress.Commands.add('newCommand', { prevSubject: 'optional' }, (subject, arg) => {
100+
subject // $ExpectType unknown
101+
arg // $ExpectType string
102+
})
103+
Cypress.Commands.add('newCommand', { prevSubject: ['optional'] }, (subject, arg) => {
104+
subject // $ExpectType unknown
105+
arg // $ExpectType string
106+
})
107+
Cypress.Commands.add('newCommand', { prevSubject: 'document' }, (subject, arg) => {
108+
subject // $ExpectType Document
109+
arg // $ExpectType string
110+
})
111+
Cypress.Commands.add('newCommand', { prevSubject: 'window' }, (subject, arg) => {
112+
subject // $ExpectType Window
113+
arg // $ExpectType string
114+
})
115+
Cypress.Commands.add('newCommand', { prevSubject: 'element' }, (subject, arg) => {
116+
subject // $ExpectType JQuery<HTMLElement>
117+
arg // $ExpectType string
118+
})
119+
Cypress.Commands.add('newCommand', { prevSubject: ['element'] }, (subject, arg) => {
120+
subject // $ExpectType JQuery<HTMLElement>
121+
arg // $ExpectType string
122+
})
123+
Cypress.Commands.add('newCommand', { prevSubject: ['element', 'document', 'window'] }, (subject, arg) => {
124+
if (subject instanceof Window) {
125+
subject // $ExpectType Window
126+
} else if (subject instanceof Document) {
127+
subject // $ExpectType Document
128+
} else {
129+
subject // $ExpectType JQuery<HTMLElement>
130+
}
131+
arg // $ExpectType string
132+
})
133+
Cypress.Commands.add('newCommand', { prevSubject: ['window', 'document', 'optional', 'element'] }, (subject, arg) => {
134+
if (subject instanceof Window) {
135+
subject // $ExpectType Window
136+
} else if (subject instanceof Document) {
137+
subject // $ExpectType Document
138+
} else if (subject) {
139+
subject // $ExpectType JQuery<HTMLElement>
140+
} else {
141+
subject // $ExpectType void
142+
}
143+
arg // $ExpectType string
144+
})
145+
Cypress.Commands.add('newCommand', (arg) => {
88146
// $ExpectType string
89147
arg
90-
return
148+
return cy.wrap(new Promise<number>((resolve) => { resolve(5) }))
149+
})
150+
Cypress.Commands.overwrite('newCommand', (originalFn, arg) => {
151+
arg // $ExpectType string
152+
originalFn // $ExpectedType Chainable['newCommand']
153+
originalFn(arg) // $ExpectType Chainable<number>
154+
})
155+
Cypress.Commands.overwrite('newCommand', function(originalFn, arg) {
156+
this // $ExpectType Context
157+
arg // $ExpectType string
158+
originalFn // $ExpectedType Chainable['newCommand']
159+
originalFn.apply(this, [arg]) // $ExpectType Chainable<number>
160+
})
161+
Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial<Cypress.TypeOptions & {sensitive: boolean}>) => {
162+
element // $ExpectType JQuery<HTMLElement>
163+
text // $ExpectType string
164+
165+
if (options && options.sensitive) {
166+
// turn off original log
167+
options.log = false
168+
// create our own log with masked message
169+
Cypress.log({
170+
$el: element,
171+
name: 'type',
172+
message: '*'.repeat(text.length),
173+
})
174+
}
175+
176+
return originalFn(element, text, options)
91177
})
92178
}
93179

0 commit comments

Comments
 (0)