-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
breaking: upgrade cy.readFile() to be a query command #25595
Conversation
6 flaky tests on run #43697 ↗︎Details:
|
Test | ||
---|---|---|
network stubbing > intercepting request > can delay and throttle a StaticResponse | ||
... > with `times` > only uses each handler N times | ||
... > stops waiting when an xhr request is canceled |
cypress/cypress.cy.js • 3 flaky tests • 5x-driver-webkit
Test | ||
---|---|---|
... > correctly returns currentRetry | ||
... > correctly returns currentRetry | ||
... > correctly returns currentRetry |
This comment has been generated by cypress-bot as a result of this project's GitHub integration settings.
a3b3c12
to
3345971
Compare
@@ -14,6 +14,14 @@ describe('src/cy/commands/files', () => { | |||
}) | |||
|
|||
describe('#readFile', () => { | |||
it('really works', () => { |
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.
Moved the simplest tests higher up in the file. It felt off that we started with detailed assertions about the interaction with the backend before just using the command plainly.
@@ -80,7 +88,7 @@ describe('src/cy/commands/files', () => { | |||
.resolves(okResponse) | |||
|
|||
cy.readFile('foo.json').then(() => { | |||
expect(retries).to.eq(1) | |||
expect(retries).to.eq(2) |
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.
When mocked, the new version always retries one more time than the non-query version. This occurs because the first invocation triggers the promise which reads the file from disk and throws an error; it is always asynchronous, even when mocked.
@@ -176,10 +178,6 @@ describe('src/cy/commands/files', () => { | |||
|
|||
this.logs = [] | |||
|
|||
cy.on('fail', () => { |
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.
Having an on('fail')
handler in the beforeEach()
block makes it hard to debug actual failures, since if you comment out a test's onFail block, the test will suddenly pass.
The only change this makes is that in some tests we see three logs rather than two, because "the stub has been called" log is included (where previously it would have been ignored because of this handler-removal).
@@ -64,11 +64,11 @@ context('cy.origin files', { browser: '!webkit' }, () => { | |||
}) | |||
|
|||
cy.shouldWithTimeout(() => { | |||
const { consoleProps } = findCrossOriginLogs('readFile', logs, 'foobar.com') | |||
const log = findCrossOriginLogs('readFile', logs, 'foobar.com') |
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.
No functional change here. While working on these changes, I found it easier to read test failures when they were of the form "unable to read consoleProps
from null
" rather than "unable to destructure intermediate object" (the actual errors are significantly more verbose and harder to read than the above).
}) | ||
} | ||
|
||
let fileResult: any = null |
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 way this now works is basically a state machine, with three pieces of state:
- The contents of the file read from disk (can be null or have a value)
- The currently active promise, waiting for a response from the server (can be null or be a promise)
- The most recent error (always has a value)
The state can be updated in three different places.
- When the query function executes (
return () => {
), if we have afileResult
, return it. Otherwise, create a newfilePromise
if we don't already have one (createFilePromise()
) and throwmostRecentError
. - When
filePromise
resolves (.then((result) => {
), we set the contents tofileResult
, and clearfilePromise
. - When
filePromise
rejects:
a. If it rejects ((.catch((err) => {
)) with "file doesn't exist", we setfileResult
to "no file exists" and clearfilePromise
.
b. Otherwise, we set the error tomostRecentError
, and clearfilePromise
. - If an upcoming assertion fails, we clear
fileResult
.
The end result is that we query the disk for a file, and throw a "timed out" error until we get back a result (including "no file exists"); if the server responds with an unexpected error, we start throwing that instead of "timed out" and retry.
If it succeeds, we start returning that file as the result.
If an upcoming assertion fails (including the implicit "file should exist" assertion), we clear the result and head back into the retry loop.
@@ -652,18 +652,21 @@ describe('src/cy/commands/assertions', () => { | |||
cy.get('button:first', { timeout: 500 }).should('have.class', 'does-not-have-class') | |||
}) | |||
|
|||
it('has a pending state while retrying for commands with onFail', (done) => { | |||
it('has a pending state while retrying for commands with onFail', function (done) { |
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.
Similar to the other test changes, this one relied on the state after the first retry; we no longer wait for a server response before "retrying" - so the test needs to be more patient.
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.
Looks good! Just one little suggestion for the error message you can take or leave. I had originally thought a command like readFile
couldn't be made into a query since queries have to be synchronous, so it's interesting to see how you've made it work.
Should this be considered a breaking change? Since readFile
is now a query, any users overwriting it as a command will no longer be able to do so.
That was definitely my initial thought as well; I spent some time while working on this PR wondering if I'd dropped the ball a bit with queries and they could be async after all, but I think the answer remains the same - it's a hard limitation in the design that can be hacked around, but not changed. I've been considering if it might be possible to provide a utility function to do this in a more generic way - the signature would be something like
where you call it with a function that returns a promise and "the error if we don't have another error" and it returns a query function. It's a bit more complicated to make it generic than it seems, which is why I didn't end up going down that route in this pull request, but I think the possibility is there.
Good call, I forgot about that side effect. Will update changelog. |
c67120c
to
29b1b7e
Compare
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
@BlueWinds Can you create a |
The base branch was changed.
Done. Also created the docs PR - cypress-io/cypress-documentation#5017. |
@@ -2,12 +2,7 @@ | |||
const fs = require('fs') |
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.
Would these changes be useful in develop
? Maybe they could be cherry-picked into another PR against develop
?
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.
@emilyrohrbough , thoughts?
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 have a branch in progress for this.
Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
Released in Cypress 13.0.0. |
Additional details
This PR updates
cy.readFile()
to be a query command. It is a breaking change because users will no longer be able to overwritereadFile
withCypress.Commands.overwrite()
. No other impact on user's tests is anticipated.cy.readFile()
already matched the behavior of other queries, by reading the file from disk until upcoming assertions pass. This update therefore is not a behavior change - it simply brings the readFile code execution path in line with other queries which do the same thing.Despite there being no user-facing changes, this update is a prerequisite for #25296. In that pull request, assertions will no longer "belong" to previous commands - they execute entirely independently. That means that the only way for a command to update the subject in response to failed assertions is to be a query - .readFile() can no longer be a special case of a "query-like command that isn't a query."
When reviewing / aiming to understand the changes, suggest starting with this comment: #25595 (comment).
Steps to test
No behavior has changed, and tests required no updates (see comments on each individual change for why it is code-clarity related and not functional).
How has the user experience changed?
Previously, if readFile timed out, users would get the message
They now instead get the message
PR Tasks
cypress-documentation
? .readFile() is now a query cypress-documentation#5017type definitions
?