Skip to content
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

Issue 194 #220

Merged
merged 11 commits into from
Nov 4, 2017
Prev Previous commit
Next Next commit
WIP begin rewriting variables and aliases
- provide much better explanations
- show multiple uses of aliases
- explain closures and why you’d use them
- debugging tips
  • Loading branch information
brian-mann committed Nov 2, 2017
commit 6e61b72e08a5d5cf8c408fcdeda6a71bf2280cad
318 changes: 311 additions & 7 deletions source/guides/core-concepts/variables-and-aliases.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,322 @@
---
title: Aliases and References
title: Variables and Aliases
comments: false
---

{% note info %}
# {% fa fa-graduation-cap %} What You'll Learn

- How Cypress's asynchronous nature makes it hard to use variables
- What Aliases are, how they simplify and enable reuse in your code
- How to use Aliases for DOM elements
- How to use Aliases for XHR requests
- How to deal with async commands
- What Aliases are, how they simplify your code
- Why you rarely need to use variables with Cypress
- How to use Aliases for objects, elements and routes
{% endnote %}

# Async Challenges
# Return Values

{% note danger 'Not Useful' %}
**You cannot assign or work with the return values** of any Cypress command because they are enqueued and run asynchronously.
{% endnote %}

```js
// ...this won't work...

// nope
const button = cy.get('button')

// nope
const form = cy.get('form')

// nope
button.click()
```

{% note success 'Do not worry!' %}
There are many simple and easy ways to reference, compare and utilize the objects that Cypress commands yield you.

Once you get the hang of async code - you'll realize you can do everything you could do synchronously, without your code doing any backflips.

This guide explores many common patterns for writing good Cypress code that can handle even the most complex situations.
{% endnote %}

## Closures

Whenever you want access to what a Cypress command yields you - you simply use {% url `.then()` then %}.

```js
cy.get('button').then(($btn) => {
// $btn is the object that the previously
// command yielded us
})
```

If you're familiar with native Promises - the Cypress `.then()` works the same way. You can continue to nest more Cypress commands inside of the `.then()`.

Each nested command has access to the work done in previous commands. This ends up reading very nicely.

```js
cy.get('button').then(($btn) => {

// store the button's text
const txt = $btn.text()

// submit a form
cy.get('form').submit()

// compare the two buttons' text
// and make sure they are different
cy.get('button').should(($btn2) => {
expect($btn2.text()).not.to.eq(txt)
})
})

// these commands run after all of the
// other previous commands have finished
cy.get(...).find(...).should(...)
```

The commands outside of the `.then()` will not run until all of the nested commands finish.

{% note info %}
By using callback functions, we've created a closure. Closures enable us to keep references around to refer to work done in previous commands.
{% endnote %}

## Debugging

Using `.then()` functions is an excellent opportunity to use `debugger`. This can help you understand the order in which commands are run. This also enables you to inspect the objects that Cypress yields you in each command.

```js
cy.get('button').then(($btn) => {
// inspect $btn <object>
debugger

cy.get('#countries').select('USA').then(($select) => {
// inspect $select <object>
debugger

cy.url().should((url) => {
// inspect the url <string>
debugger

$btn // is still available
$select // is still available too

})
})
})

```

## Variables

Typically in Cypress - you hardly need to ever use `const`, `let`, or `var`. When using closures - you'll always have access to the objects that were yielded to you without assigning them.

The one exception to this rule is when you are dealing with mutable objects (that change state). When things change state, you often want to compare an object's previous value to the next value.

Here's a great use case for a `const`.

```html
<button>increment</button>

you clicked button <span id='num'>0</span> times
```

```js
// app code
let count = 0

$('button').on('click', function(){
$('#num').text(count += 1)
})
```

```js
// cypress test code
cy.get('#num').then(($span) => {
// capture what num is right now
const num1 = parseFloat($span.text())

cy.get('button').click().then(() => {
// now capture it again
const num2 = parseFloat($span.text())

// make sure its what we expected
expect(num2).to.eq(num1 + 1)
})
})
```

The reason for using `const` is because the the `$span` object is mutable. Whenever you have mutable objects and you're trying to compare them, you'll need to store their values. Using `const` is a perfect way to do that.

# Aliases

Using `.then()` callback functions to access the previous command values is great - but what happens when you're running code in hooks like `before` or `beforeEach`?

```js
beforeEach(function () {
cy.button().then(($btn) => {
const text = $btn.text()
})
})

it('does not have access to text', function () {
// how do we get access to text ?!?!
})
```

How will we get access to `text`?

We could make our code do some ugly backflips using `let` to get access to it.

{% note warning 'Do not do this' %}
This code below is just for demonstration.
{% endnote %}

```js
describe('a suite', function () {
// this creates a closure around
// 'text' so we can access it
let text

beforeEach(function () {
cy.button().then(($btn) => {
// redefine text reference
text = $btn.text()
})
})

it('does have access to text', function () {
text // now this is available to us
})
})
```

Fortunately, you don't have to make your code do backflips. Cypress makes it easy to handle these situations.

{% note success 'Introducing Aliases' %}
Aliases are a powerful construct in Cypress that have many uses. We'll explore each of their capabilities below.

At first, we'll use them to make it easy to share objects between your hooks and your tests.
{% endnote %}

## Sharing Context

Sharing context is simplest way to use aliases.

To alias something you'd like to share - you use the command called: {% url `.as()` as %}.

Let's look at our previous example with aliases.

```js
beforeEach(function () {
// alias the $btn.text() as 'text'
cy.get('button').invoke('text').as('text')
})

it('has access to text', function () {
this.text // is now available
})
```

Under the hood, aliasing basic objects and primitives like this utilizes mocha's shared `context` here.

Contexts are shared across all applicable hooks for each test and are automatically cleaned up after each test.

```js
describe('parent', function () {
beforeEach(function () {
cy.wrap('one').as('a')
})

context('child', function () {
beforeEach(function () {
cy.wrap('two').as('b')
})

describe('grandchild', function () {
beforeEach(function () {
cy.wrap('three').as('c')
})

it('can access all aliases as properties', function () {
expect(this.a).to.eq('one') // true
expect(this.b).to.eq('two') // true
expect(this.c).to.eq('three') // true
})
})
})
})
```

The most common use case for sharing context is when dealing with {% url `cy.fixture()` fixture %}.

Often times you may load in a fixture in a `beforeEach` hook but want to utilize the values in your tests.

```js
beforeEach(function () {
// alias the users fixtures
cy.fixture('users.json').as('users')
})

it('utilize users in some way', function () {
// access the users property
const user = this.users[0]

// make sure the header contains the first
// users name
cy.get('header').should('contain', user.name)
})
```

{% note danger 'Watch out for async commands' %}
Do not forget that **Cypress commands are async**!

You cannot use a `this.*` reference until the `.as()` command runs.
{% endnote %}

```js
it('is not using aliases correctly', function () {
cy.fixture('users.json').as('users')

// nope this won't work
//
// this.users is not defined
// because the 'as' command has only
// been enqueued - it has not ran yet
const user = this.users[0]
})
```

The same principles we introduced many times before apply to this situation. If you want to what a command yields, you'll have to do it in a closure using a {% url `.then()` then %}.

```js
// yup all good
cy.fixture('users.json').then((users) => {
// now we can avoid the alias altogether
// and just use a callback function
const user = users[0]

// passes
cy.get('header').should('contain', user.name)
})
```

## Elements

## Routes

TODO
- WHY DO WE USE 'FUNCTION' IN ALL OF OUR EXAMPLES?
- WHAT DO WE SAY IN THE 'GET' DOCS ABOUT USING IT FOR PRIMITIVES?




<!-- ...OLD CRAPPY CONTENT BELOW...

In Cypress - you rarely have to **ever** use `const`, `let`, or `var`. If you are finding yourself reaching for these common idioms - there is likely a better and easier way.



Because all commands in Cypress are asynchronous, it can be difficult to refer back to work done in previous commands. Cypress provides a special DSL for doing just that, and it's called Aliasing. Let's look at some examples to illustrate what we mean.

Expand Down Expand Up @@ -158,4 +462,4 @@ cy.get('.success').contains('User successfully created!')
1. Waiting for an explicit route reference is less flaky. Instead of waiting for an arbitrary period of time, waiting for a specific aliased route is much more predictable.
2. Cypress will resolve if there's already been a request that matches the alias.
3. The actual XHR request object will be yielded to you as the next subject.
4. Errors are more obvious.
4. Errors are more obvious. -->