-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: Update type definitions to match API more closely #1048
chore: Update type definitions to match API more closely #1048
Conversation
@bahmutov This is the next part. I kept the same The following is how it is currently typed: cy.wrap({ foo: 'bar' })
.then(s => {
s // $ExpectType { foo: string }
return s
})
.then(s => {
s // $ExpectType { foo: string }
})
.its('foo')
.then(s => {
s // $ExpectType string
}) |
cli/types/index.d.ts
Outdated
children(options?: LoggableTimeoutable): Chainable | ||
children(selector: string, options?: LoggableTimeoutable): Chainable | ||
children<E extends Node = HTMLElement>(options?: Partial<Loggable & Timeoutable>): Chainable<JQuery<E>> | ||
children<K extends keyof HTMLElementTagNameMap>(selector: K, options?: Partial<Loggable & Timeoutable>): Chainable<JQuery<HTMLElementTagNameMap[K]>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line allows for tag-based selectors to automatically infer the right element. For example:
cy.get('form')
.then(el => {
el // $ExpectType JQuery<HTMLFormElement>
})
.children('input')
.then(el => {
el // $ExpectType JQuery<HTMLInputElement>
})
Of course these can be overridden:
cy.get<HTMLFormElement>('.main-form')
.then(el => {
el // $ExpectType JQuery<HTMLFormElement>
})
.children<HTMLInputElement>('.input')
.then(el => {
el // $ExpectType JQuery<HTMLInputElement>
})
contains(num: number | RegExp): Chainable | ||
contains(selector: string, text: string, options?: LoggableTimeoutable): Chainable | ||
contains(content: string | number | RegExp): Chainable<Subject> | ||
contains<E extends Node = HTMLElement>(content: string | number | RegExp): Chainable<JQuery<E>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These generic overrides are for when the user knows what the element is going to be and needs access to special properties or methods on the HTML element. It is probably going to be pretty rare, but may be required in rare circumstances (maybe a hacked file upload that requires an HTMLInputElement
type.
The fallback for all generic overrides is HTMLElement
which contains all the generic methods and properties. Also the Subject
returned by all Element selectors is a JQuery
object which helps to normalize a lot of calls.
* @see https://on.cypress.io/filter | ||
*/ | ||
filter(selector: string, options?: LoggableTimeoutable): Chainable | ||
filter<K extends keyof HTMLElementTagNameMap>(selector: K, options?: Partial<Loggable & Timeoutable>): Chainable<JQuery<HTMLElementTagNameMap[K]>> // automatically returns the correct HTMLElement type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A way of getting the right element without using a generic would be to use the .filter
method:
cy.get('.input') // HTMLElement since we can't infer type
.filter('input') // HTMLInputElement since 'input' is a keyof match
.then($input => {
$el // $ExpectType JQuery<HTMLInputElement>
})
That would also always make sure you have an <input>
or else the test will fail before it gets there. I prefer this over generic overrides since overriding generics is basically overriding the type system (possible runtime error in that case)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if, given that info, that should be the way to get a specific element type? I don't like seeing generics in non-library code - it is just too easy to misuse.
Generics would still be required in areas we can't infer based on input (like fixture contents or results from a .as
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I bet sometimes as <...>
would still be needed, unfortunately (or fortunately if you think of the flexibility)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think generic overrides are necessary sometimes. I think it is a good practice to avoid the requirement. I've seen developers introduce runtime errors by incorrectly overriding them. It can be tricky because type errors can produce very difficult "type traces".
cli/types/index.d.ts
Outdated
* @see https://on.cypress.io/not | ||
*/ | ||
not(selector: string, options?: LoggableTimeoutable): Chainable | ||
not<E extends Node = HTMLElement>(selector: string, options?: Partial<Loggable & Timeoutable>): Chainable<JQuery<E>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't narrow the type down much with a .not
. I'm not even sure a generic should be used here. HTMLElement
is what all elements would have in common. I think I'll remove the generic from this.
cli/types/index.d.ts
Outdated
* @see https://on.cypress.io/spread | ||
*/ | ||
spread(fn: (...args: any[]) => any): Chainable | ||
spread(fn: (...args: any[]) => void): Chainable<Subject> // Can't type this any better without breaking up Chainable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These types aren't quite right. I had them working with commands being broken down. Without that, I don't have a way of typing a Subject
that is an array as just a Subject again.
I may have to either add a ChainableArray
interface or leave anything hitting .spread
as any
cli/types/index.d.ts
Outdated
/** | ||
* Chainable interface with stronger typing for array subjects | ||
*/ | ||
interface ChainableArray<Subject> extends Omit<Chainable<Subject[]>, 'and' | 'as' | 'each' | 'should' | 'then'> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added ChainableArray<Subject>
to properly capture types for .each
and .spread
and theoretically any Command that is meant to work on arrays. Without this, .each
and .spread
would be forced to resort to any
for parameters, putting more type burden on the application author.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Omit
part basically takes the whole Chainable
interface and removes some keys ('and', 'as', etc in this case). I'm defining the ChainableArray
interface to be the same as Chainable
except a few methods are replaced with different type definitions. I could simply extend but then the wrong overrides would be chosen.
@@ -0,0 +1,1470 @@ | |||
// Samples taken from the cypress kitchen sink example (https://github.com/cypress-io/cypress-example-kitchensink) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this file automatically passing dtslint because there are no $ExpectType
assertions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dtslint
will look for compilation errors as well - unless you use // $ExpectError
on the previous line.
You can see the TS compilation error it caught here: https://circleci.com/gh/cypress-io/cypress/7985
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is really cool - you can make assertions if you want errors for something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love it!!!
@NicholasBoll how are you so good at TypeScript? Any resources that you can recommend for @brian-mann and me?
@bahmutov I find the blog by Marius Schulz to be an excellent way to learn TypeScript features: https://blog.mariusschulz.com/. I've been keeping up on the release notes of TypeScript as well (I do that with Cypress too 😄) Really it takes some practice. I forced myself to understand type definitions. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts is a good example of a small, but dense definition file. Also just spending (wasting?) time trying to make a type better. Sometimes it panned out, sometimes not and a dizzying rabbit hole sometimes. I didn't understand different modules types very well until working on these type definitions - and how to mix modules types (TypeScript detects module mode based on the use of |
What do you think about the split of
|
cli/types/index.d.ts
Outdated
*/ | ||
spread(fn: (...args: any[]) => any): Chainable | ||
spy(): sinon.SinonSpy & Chainable<sinon.SinonSpy> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brian-mann The docs say cy.spy
returns sinon.spy
, but I also see an alias. Here I'm adding all of Chainable to the return, but I'm guessing that isn't correct. I could find the implementation to confirm.
Does cy.spy()
and cy.stub
have only a special non-chainable as
method added to them and nothing else? If so, I should update this type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. They only have a single .as
method added to them. This brings them into parity with other Cypress idioms. They show up in the Command Log highlighted with the alias, and it also enables you to use them with cy.get('@alias')
Here's the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW there is also a log
method which enables you to turn off Command Log logging. This is necessary in situations where the spied / stubbed function is called hundreds of times and would lock up the UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Not sure what to call the interface. Perhaps Agent
? It looks like it returns a Spy/Stub with a chainable interface with just .log
and .as
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. I see that withArgs
was overridden, so the Agent
interface does the same thing (to allow the continuation of the Agent
chainable
I would like to get rid of Background:
I'm just not a fan of the complexity introduced by Workarounds:A developer can get around this in a strongly typed way without having to override generics or specify types: Spread:
cy.wrap([1, 2])
.then(([first, second]) => {
first // $ExpectType number
}) Each:Don't use cy.wrap([1, 2])
.then(subject => {
Cypress._.each(subject, (item) => {
item // $ExpectType number
})
}) The primary use case for cy.wrap([1, 2])
.each((item: number) => {
item // $ExpectType number
}) |
Unfortunately The lodash method is purely synchronous - whereas |
I see the implementation of If the Subject is a cy.get('.someSelector')
.each(($el, index, list) => {
$el // $ExpectType JQuery<HTMLElement>
index // $ExpectType number
list // $ExpectType HTMLElement[]
}) If the subject isn't a JQuery-wrapped element, the type must be overridden (since it could be an array of anything): cy.wrap([1, 2, 3])
.each((item: number, index, list: number[]) => {
item // $ExpectType number
index // $ExpectType number
list // $ExpectType number[]
})
namespace Cypress {
interface Chainable<Subject> {
myMethod<S>(fn: () => S): Chainable<S>
myMethodThatReturnsAnArray<S>(fn: () => S[]): ChainableArray<S>
}
}
Cypress.add('myMethod', /** ... */)
Cypress.add('myMethodThatReturnsAnArray', /** */) I guess that isn't too bad. It is up to you |
I am fine with the user having to provide types once in a while, like you showed
|
Can this be merged in? I see incremental work on it, so wasn't sure if its all ready to go or not. I see that this PR is closing: #1050 But from the latest commits it appears it's doing more than just what @bahmutov listed on it. Can one of you provide me with a summary of what it's doing so I can write the changelog. Trying to get |
Thanks @bahmutov. I think that is fine for this PR. In #1040 you asked me to break up type definitions into separate and targeted PRs. This one focused on adding missing method APIs and updating existing method APIs. This PR leaves the @brian-mann One of the commits removed the scope creep to add another interface (that should be part of a different PR). The other fixed the #1050 summarises the changes well. It is ready to be merged. |
Thanks @brian-mann and @bahmutov for the feedback to make the type definitions as accurate as possible. |
This reverts commit e9aa9a3. This brings back TypeScript definition that is necessary for Cypress 1.1.4 to pass TS linter
Just a note for @brian-mann or @jennifer-shehane or whoever does the release notes - this change is not breaking for anyone simply consuming Cypress using JavaScript or TypeScript. It will be a breaking change for anyone extending Cypress in TypeScript. The signature of the Chainer interface changed from Maybe a note: Before: declare namespace Cypress {
interface Chainer {
myMethod(): Cypress.Chainer
}
} After: declare namespace Cypress {
interface Chainer<Subject> {
myMethod(): Cypress.Chainer<Subject>
}
} |
Yeah these kinds of things are definitely critical for us to note.
Appreciate it.
…On Thu, Dec 14, 2017 at 9:01 PM, Nicholas Boll ***@***.***> wrote:
Just a note for @brian-mann <https://github.com/brian-mann> or
@jennifer-shehane <https://github.com/jennifer-shehane> or whoever does
the release notes - this change is not breaking for anyone simply consuming
Cypress using JavaScript or TypeScript. It will be a breaking change for
anyone *extending* Cypress in TypeScript. The signature of the Chainer
interface changed from Chainer to Chainer<Subject>
Maybe a note:
Before:
namespace Cypress {
interface Chainer {
myMethod(): Cypress.Chainer
}
}
After:
namespace Cypress {
interface Chainer<Subject> {
myMethod(): Cypress.Chainer<Subject>
}
}
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1048 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABNc8K_Tv9IJ0OqJ5VjYF-33A66lRtvWks5tAdL2gaJpZM4Q73mi>
.
|
I've tried to reduce breaking changes when working with type definitions. Sorry for not mentioning earlier. I'll be sure to add notes for the change log as part of type PRs. I think Cypress is great and I want everyone using it to agree. Honestly, I didn't know people were using TypeScript + Cypress. The |
Addresses some of the suggestions in #1040 and closes #1050
This PR finishes out the Cypress API types using the existing
Chainable
interface. While not technically correct according to the implementation ofCommands
, it passes the kitchen sink example.I have code pending for breaking up the Command interface into smaller and more restrictive interfaces, but that will probably require more discussion.
Changes:
mocha
typeschai
types (withsinon-chai
andchai-jquery
extensions)Chainable
toChainable<Subject>
ChainableArray<Subject>
for dealing with Array subjects. Without this,.each
and.spread
were impossible to type without resorting toany