From f376684e4a6b5985d9cc2f2f5d7e2eaef241c89f Mon Sep 17 00:00:00 2001 From: BlueWinds Date: Thu, 7 Oct 2021 12:48:18 -0700 Subject: [PATCH 1/3] More clearly explain that cypress commands are not promises --- .../core-concepts/conditional-testing.md | 22 ---------- .../core-concepts/introduction-to-cypress.md | 43 ++++++++----------- 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/content/guides/core-concepts/conditional-testing.md b/content/guides/core-concepts/conditional-testing.md index a0c5a6e23d..750aeafdeb 100644 --- a/content/guides/core-concepts/conditional-testing.md +++ b/content/guides/core-concepts/conditional-testing.md @@ -557,25 +557,3 @@ on other commands. If you cannot accurately know the state of your application then no matter what programming idioms you have available - **you cannot write 100% deterministic tests**. - -Still not convinced? - -Not only is this an anti-pattern, but it's an actual logical fallacy. - -You may think to yourself... okay fine, but 4 seconds - man that's not enough. -Network requests could be slow, let's bump it up to 1 minute! - -Even then, it's still possible a WebSocket message could come in... so 5 -minutes! - -Even then, not enough, it's possible a `setTimeout` could trigger... 60 minutes. - -As you approach infinity your confidence does continue to rise on the chances -you could prove the desired state will be reached, but you can never prove it -will. Instead you could theoretically be waiting for the heat death of the -universe for a condition to come that is only a moment away from happening. -There is no way to prove or disprove that it _may_ conditionally happen. - -You, the test writer, must know ahead of time what your application is -programmed to do - or have 100% confidence that the state of a mutable object -(like the DOM) has stabilized in order to write accurate conditional tests. diff --git a/content/guides/core-concepts/introduction-to-cypress.md b/content/guides/core-concepts/introduction-to-cypress.md index 7736c7fc03..6230d3e2db 100644 --- a/content/guides/core-concepts/introduction-to-cypress.md +++ b/content/guides/core-concepts/introduction-to-cypress.md @@ -852,7 +852,7 @@ the timeout is reached, the test will fail. -### Commands Are Promises +### Commands Are Not Promises This is the big secret of Cypress: we've taken our favorite pattern for composing JavaScript code, Promises, and built them right into the fabric of @@ -899,18 +899,10 @@ it('changes the URL when "awesome" is clicked', () => { ``` Big difference! In addition to reading much cleaner, Cypress does more than -this, because **Promises themselves have no concepts of -[retry-ability](/guides/core-concepts/retry-ability)**. - -Without [**retry-ability**](/guides/core-concepts/retry-ability), assertions -would randomly fail. This would lead to flaky, inconsistent results. This is -also why we cannot use new JS features like `async / await`. - -Cypress cannot yield you primitive values isolated away from other commands. -That is because Cypress commands act internally like an asynchronous stream of -data that only resolve after being affected and modified **by other commands**. -This means we cannot yield you discrete values in chunks because we have to know -everything about what you expect before handing off a value. +this. Almost all commands come with built-in +[retry-ability](/guides/core-concepts/retry-ability)**. Without +[**retry-ability**](/guides/core-concepts/retry-ability), assertions +would randomly fail. This would lead to flaky, inconsistent results. These design patterns ensure we can create **deterministic**, **repeatable**, **consistent** tests that are **flake free**. @@ -930,14 +922,15 @@ read our [Core Concept Guide](/guides/core-concepts/variables-and-aliases). -### Commands Are Not Promises +### Commands Are Really Not Promises -The Cypress API is not an exact 1:1 implementation of Promises. They have -Promise like qualities and yet there are important differences you should be +The Cypress API is similar to promises visually, but it is actually a command +queue, managing async coordination for you. Commands have some Promise like +qualities, but yet there are important differences you should be aware of. 1. You cannot **race** or run multiple commands at the same time (in parallel). -2. You cannot 'accidentally' forget to return or chain a command. +2. You cannot accidentally forget to return or chain a command. 3. You cannot add a `.catch` error handler to a failed command. There are _very_ specific reasons these limitations are built into the Cypress @@ -994,9 +987,9 @@ return fs.readFile('/foo.txt', 'utf8').then((txt) => { ``` The reason this is even possible to do in the Promise world is because you have -the power to execute multiple asynchronous actions in parallel. Under the hood, -each promise 'chain' returns a promise instance that tracks the relationship -between linked parent and child instances. +the power to execute multiple asynchronous actions in parallel. Each promise +'chain' returns a promise instance that tracks the relationship between linked +parent and child instances. Because Cypress enforces commands to run _only_ serially, you do not need to be concerned with this in Cypress. We enqueue all commands onto a _global_ @@ -1020,12 +1013,10 @@ You might be wondering: > does (or doesn't) exist, I choose what to do? The problem with this question is that this type of conditional control flow -ends up being non-deterministic. This means it's impossible for a script (or -robot), to follow it 100% consistently. - -In general, there are only a handful of very specific situations where you _can_ -create control flow. Asking to recover from errors is actually the same as -asking for another `if/else` control flow. +ends up being non-deterministic. This means different test runs may behave +differently, which makes them less deterministic and consistent. In general, +there are only a handful of very specific situations where you _can_ +create control flow using Cypress commands. With that said, as long as you are aware of the potential pitfalls with control flow, it is possible to do this in Cypress! From 28926d25d3abb7aaf0a45e6342e68fc0d99b869e Mon Sep 17 00:00:00 2001 From: BlueWinds Date: Tue, 12 Oct 2021 09:50:50 -0700 Subject: [PATCH 2/3] Further rewrite introduction to commands --- .../core-concepts/introduction-to-cypress.md | 126 +++--------------- 1 file changed, 17 insertions(+), 109 deletions(-) diff --git a/content/guides/core-concepts/introduction-to-cypress.md b/content/guides/core-concepts/introduction-to-cypress.md index 6230d3e2db..22cb0821ce 100644 --- a/content/guides/core-concepts/introduction-to-cypress.md +++ b/content/guides/core-concepts/introduction-to-cypress.md @@ -852,95 +852,40 @@ the timeout is reached, the test will fail. -### Commands Are Not Promises +### The Cypress Command Queue -This is the big secret of Cypress: we've taken our favorite pattern for -composing JavaScript code, Promises, and built them right into the fabric of -Cypress. Above, when we say we're enqueuing actions to be taken later, we could -restate that as "adding Promises to a chain of Promises". +While the API may look similar to Promises, with it's `then()` syntax, Cypress +commands are not promises - they are serial commands passed into a central +queue, to be executed asynchronously at a later date. These commands are +designed to deliver deterministic, repeatable and consistent tests. -Let's compare the prior example to a fictional version of it as raw, -Promise-based code: - -#### Noisy Promise demonstration. Not valid code. - -```js -it('changes the URL when "awesome" is clicked', () => { - // THIS IS NOT VALID CODE. - // THIS IS JUST FOR DEMONSTRATION. - return cy - .visit('/my/resource/path') - .then(() => { - return cy.get('.awesome-selector') - }) - .then(($element) => { - // not analogous - return cy.click($element) - }) - .then(() => { - return cy.url() - }) - .then((url) => { - expect(url).to.eq('/my/resource/path#awesomeness') - }) -}) -``` - -#### How Cypress really looks, Promises wrapped up and hidden from us. - -```javascript -it('changes the URL when "awesome" is clicked', () => { - cy.visit('/my/resource/path') - - cy.get('.awesome-selector').click() - - cy.url().should('include', '/my/resource/path#awesomeness') -}) -``` - -Big difference! In addition to reading much cleaner, Cypress does more than -this. Almost all commands come with built-in +Almost all commands come with built-in [retry-ability](/guides/core-concepts/retry-ability)**. Without [**retry-ability**](/guides/core-concepts/retry-ability), assertions would randomly fail. This would lead to flaky, inconsistent results. -These design patterns ensure we can create **deterministic**, **repeatable**, -**consistent** tests that are **flake free**. - -Cypress is built using Promises that come from -[Bluebird](http://bluebirdjs.com/). However, Cypress commands do not return -these typical Promise instances. Instead we return what's called a `Chainer` -that acts like a layer sitting on top of the internal Promise instances. - -For this reason you cannot **ever** return or assign anything useful from -Cypress commands. - -If you'd like to learn more about handling asynchronous Cypress Commands please -read our [Core Concept Guide](/guides/core-concepts/variables-and-aliases). +While Cypress is built using Promises that come from +[Bluebird](http://bluebirdjs.com/), these are not what we expose +as commands and assertions on `cy`. If you'd like to learn more about +handling asynchronous Cypress Commands please read our +[Core Concept Guide](/guides/core-concepts/variables-and-aliases). -### Commands Are Really Not Promises - -The Cypress API is similar to promises visually, but it is actually a command -queue, managing async coordination for you. Commands have some Promise like -qualities, but yet there are important differences you should be -aware of. +Commands also have some design choices that developers used to promise-based +testing may find unexpected. They are intentional decisions on Cypress' part, +not technical limitations. 1. You cannot **race** or run multiple commands at the same time (in parallel). -2. You cannot accidentally forget to return or chain a command. -3. You cannot add a `.catch` error handler to a failed command. - -There are _very_ specific reasons these limitations are built into the Cypress -API. +2. You cannot add a `.catch` error handler to a failed command. -The whole intention of Cypress (and what makes it very different from other +The whole purpose of Cypress (and what makes it very different from other testing tools) is to create consistent, non-flaky tests that perform identically from one run to the next. Making this happen isn't free - there are some trade-offs we make that may initially seem unfamiliar to developers accustomed -to working with Promises. +to working with Promises or other libraries. Let's take a look at each trade-off in depth: @@ -964,43 +909,6 @@ manner in order to create consistency. Because integration and e2e tests primarily mimic the actions of a real user, Cypress models its command execution model after a real user working step by step. -#### You cannot accidentally forget to return or chain a command - -In real promises it's very easy to 'lose' a nested Promise if you don't return -it or chain it correctly. - -Let's imagine the following Node code: - -```js -// assuming we've promisified our fs module -return fs.readFile('/foo.txt', 'utf8').then((txt) => { - // oops we forgot to chain / return this Promise - // so it essentially becomes 'lost'. - // this can create bizarre race conditions and - // bugs that are difficult to track down - fs.writeFile('/foo.txt', txt.replace('foo', 'bar')) - - return fs.readFile('/bar.json').then((json) => { - // ... - }) -}) -``` - -The reason this is even possible to do in the Promise world is because you have -the power to execute multiple asynchronous actions in parallel. Each promise -'chain' returns a promise instance that tracks the relationship between linked -parent and child instances. - -Because Cypress enforces commands to run _only_ serially, you do not need to be -concerned with this in Cypress. We enqueue all commands onto a _global_ -singleton. Because there is only ever a single command queue instance, it's -impossible for commands to ever be _'lost'_. - -You can think of Cypress as "queueing" every command. Eventually they'll get run -and in the exact order they were used, 100% of the time. - -There is no need to ever `return` Cypress commands. - #### You cannot add a `.catch` error handler to a failed command In Cypress there is no built in error recovery from a failed command. A command From 77fdca4bea734a2c51fbded022e5cf1c79d4265e Mon Sep 17 00:00:00 2001 From: Blue F Date: Wed, 13 Oct 2021 13:52:40 -0700 Subject: [PATCH 3/3] Update content/guides/core-concepts/introduction-to-cypress.md Co-authored-by: Emily Rohrbough --- content/guides/core-concepts/introduction-to-cypress.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/guides/core-concepts/introduction-to-cypress.md b/content/guides/core-concepts/introduction-to-cypress.md index 22cb0821ce..da6a930379 100644 --- a/content/guides/core-concepts/introduction-to-cypress.md +++ b/content/guides/core-concepts/introduction-to-cypress.md @@ -874,7 +874,7 @@ handling asynchronous Cypress Commands please read our -Commands also have some design choices that developers used to promise-based +Commands also have some design choices that developers who are used to promise-based testing may find unexpected. They are intentional decisions on Cypress' part, not technical limitations.