diff --git a/content/guides/end-to-end-testing/testing-your-app.md b/content/guides/end-to-end-testing/testing-your-app.md
index 1ec9e65428..a26e3ef039 100644
--- a/content/guides/end-to-end-testing/testing-your-app.md
+++ b/content/guides/end-to-end-testing/testing-your-app.md
@@ -410,111 +410,101 @@ You'll likely also want to test your login UI for:
Each of these likely requires a full blown e2e test.
-Now, once you have your login completely tested - you may be tempted to think:
+#### Reusing the login code
+
+At this point there's nothing stopping you copying and pasting the login code
+above into every one of your tests that needs an authenticated user. Or you
+could even put all your tests in one big spec file and put the login code in a
+`beforeEach` block. But neither of those approaches is particularly
+maintainable, and they're certainly not very elegant. A much better solution is
+to write a custom `cy.login()` [command](/api/cypress-api/custom-commands).
+
+Custom commands allow you to easily encapsulate and reuse Cypress test logic.
+They let you add your own functionality to your test suite and then use it with
+the same
+[chainable and asynchronous API](guides/core-concepts/introduction-to-cypress#The-Cypress-Command-Queue)
+as the built-in Cypress commands. Lets make the above login example a custom
+command and add it to `cypress/support/commands.js` so it can be leveraged in
+any spec file:
-> "...okay great! Let's repeat this login process before every single test!"
-
-
-
-
-
-Do not use **your UI** to login before each test.
-
-
-
-Let's investigate and tease apart why.
-
-#### Bypassing your UI
-
-When you're writing tests for a very **specific feature**, you _should_ use your
-UI to test it.
-
-But when you're testing _another area of the system_ that relies on a state from
-a previous feature: **do not use your UI to set up this state**.
+```js
+// In cypress/support/commands.js
-Here's a more robust example:
+Cypress.Commands.add('login', (username, password) => {
+ cy.visit('/login')
-Imagine you're testing the functionality of a **Shopping Cart**. To test this,
-you need the ability to add products to that cart. Well where do the products
-come from? Should you use your UI to login to the admin area, and then create
-all of the products including their descriptions, categories, and images? Once
-that's done should you then visit each product and add each one to the shopping
-cart?
+ cy.get('input[name=username]').type(username)
-No. You shouldn't do that.
+ // {enter} causes the form to submit
+ cy.get('input[name=password]').type(`${password}{enter}`, { log: false })
-
+ // we should be redirected to /dashboard
+ cy.url().should('include', '/dashboard')
-
+ // our auth cookie should be present
+ cy.getCookie('your-session-cookie').should('exist')
-Don't use your UI to build up state! It's enormously slow, cumbersome, and
-unnecessary.
+ // UI should reflect this user being logged in
+ cy.get('h1').should('contain', username)
+})
-Read about [best practices](/guides/references/best-practices) here.
+// In your spec file
-
+it('does something on a secured page', function () {
+ const { username, password } = this.currentUser
+ cy.login(username, password)
-Using your UI to **log in** is the _exact same scenario_ as what we described
-previously. Logging in is a prerequisite of state that comes before all of your
-other tests.
+ // ...rest of test
+})
+```
-Because Cypress isn't Selenium, we can actually take a huge shortcut here and
-skip needing to use our UI by using [`cy.request()`](/api/commands/request).
+#### Improving performance
-Because [`cy.request()`](/api/commands/request) automatically gets and sets
-cookies under the hood, we can actually use it to build up state without using
-your browser's UI, yet still have it perform exactly as if it came from the
-browser!
+You're probably wondering what happened to our advice about logging in "only
+once". The custom command above will work just fine for testing your secured
+pages, but if you have more than a handful of tests, logging in before every
+test is going to increase the overall run time of your suite.
-Let's revisit the example from above but assume we're testing some other part of
-the system.
+Luckily, Cypress provides the [`cy.session()`](/api/commands/session) command, a
+powerful performance tool that lets you cache the browser context associated
+with your user and reuse it for multiple tests without going through multiple
+login flows! Let's modify the custom `cy.login()` command from our previous
+example to use `cy.session()`:
```js
-describe('The Dashboard Page', () => {
- beforeEach(() => {
- // reset and seed the database prior to every test
- cy.exec('npm run db:reset && npm run db:seed')
-
- // seed a user in the DB that we can control from our tests
- // assuming it generates a random password for us
- cy.request('POST', '/test/seed/user', { username: 'jane.lane' })
- .its('body')
- .as('currentUser')
- })
-
- it('logs in programmatically without using the UI', function () {
- // destructuring assignment of the this.currentUser object
- const { username, password } = this.currentUser
-
- // programmatically log us in without needing the UI
- cy.request('POST', '/login', {
- username,
- password,
- })
-
- // now that we're logged in, we can visit
- // any kind of restricted route!
- cy.visit('/dashboard')
-
- // our auth cookie should be present
- cy.getCookie('your-session-cookie').should('exist')
-
- // UI should reflect this user being logged in
- cy.get('h1').should('contain', 'jane.lane')
- })
+Cypress.Commands.add('login', (username, password) => {
+ cy.session(
+ username,
+ () => {
+ cy.visit('/login')
+ cy.get('input[name=username]').type(username)
+ cy.get('input[name=password]').type(`${password}{enter}`, { log: false })
+ cy.url().should('include', '/dashboard')
+ cy.get('h1').should('contain', username)
+ },
+ {
+ validate: () => {
+ cy.getCookie('your-session-cookie').should('exist')
+ },
+ }
+ )
})
```
-Do you see the difference? We were able to login without needing to actually use
-our UI. This saves an enormous amount of time visiting the login page, filling
-out the username, password, and waiting for the server to redirect us _before
-every test_.
+
+
+
-Because we previously tested the login system end-to-end without using any
-shortcuts, we already have 100% confidence it's working correctly.
+If your app implements login via a third-party authentication provider such as
+[Auth0](https://auth0.com/) or [Okta](https://www.okta.com/), you can use the
+[`cy.origin()`](/api/commands/origin) command to include their login pages as
+part of your authentication tests.
+
+
-Use the methodology above when working with any area of your system that
-requires the state to be set up elsewhere. Just remember - don't use your UI!
+There's a lot going on here that's out of the scope for this introduction.
+Please check out the [`cy.session()`](/api/commands/session) documentation for a
+more in-depth explanation.