Gleb Bahmutov

Distinguished Engineer

@bahmutov

WEBCAST

Build, Deploy, and Test

Ask Questions at Sli.do event code #cy-netlify

Jason Lengstorf

Principal Developer Experience Engineer

@jlengstorf

AGENDA

  • Tested E-commerce site

  • Deploying to Netlify

  • Testing the deployed site

  • Testing before the deploy

  • Writing Netlify Build plugin

  • Q & A

    • Sli.do event code #cy-netlify

E-commerce Store

  • Items

  • Cart

  • Stripe checkout

End-to-end test

cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log("**men' items**")
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3).first().click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')

// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
  method: 'POST',
  pathname: '/.netlify/functions/create-payment-intent',
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()

cy.log('**cart page**')
cy.location('pathname').should('equal', '/cart')
// confirm the products in the cart
cy.get('[data-cy=item-in-cart]')
  .should('have.length', 1)
  .first()
  .within(() => {
    cy.contains('.product-size', 'Size: Large')
    cy.contains('[data-cy=item-quantity]', 2)
  })
cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log("**men' items**")
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3).first().click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')

// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
  method: 'POST',
  pathname: '/.netlify/functions/create-payment-intent',
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()

cy.log('**cart page**')
cy.location('pathname').should('equal', '/cart')
// confirm the products in the cart
cy.get('[data-cy=item-in-cart]')
  .should('have.length', 1)
  .first()
  .within(() => {
    cy.contains('.product-size', 'Size: Large')
    cy.contains('[data-cy=item-quantity]', 2)
  })
cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log("**men' items**")
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3).first().click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')

// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
  method: 'POST',
  pathname: '/.netlify/functions/create-payment-intent',
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()

cy.log('**cart page**')
cy.location('pathname').should('equal', '/cart')
// confirm the products in the cart
cy.get('[data-cy=item-in-cart]')
  .should('have.length', 1)
  .first()
  .within(() => {
    cy.contains('.product-size', 'Size: Large')
    cy.contains('[data-cy=item-quantity]', 2)
  })
cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log("**men' items**")
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3).first().click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')

// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
  method: 'POST',
  pathname: '/.netlify/functions/create-payment-intent',
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()

cy.log('**cart page**')
cy.location('pathname').should('equal', '/cart')
// confirm the products in the cart
cy.get('[data-cy=item-in-cart]')
  .should('have.length', 1)
  .first()
  .within(() => {
    cy.contains('.product-size', 'Size: Large')
    cy.contains('[data-cy=item-quantity]', 2)
  })
cy.visit('/')
cy.contains('nav li', 'Men').click()

cy.log("**men' items**")
cy.location('pathname').should('equal', '/men')

// let's not go crazy here, $30 is plenty!
// we change the max value by setting the value
// of the slider and then triggering the "input" event
cy.get('#pricerange').invoke('val', 30).trigger('input')

// check the number of items < $30
cy.get('.content .item').should('have.length', 3).first().click()

cy.log('**item page**')
cy.location('pathname').should('match', /\/product\//)
cy.contains('.product-info h1', 'Armin Basilio')
cy.get('.size-picker').select('Large')

// this jacket is so lovely, let's order 2
cy.contains('.update-num', '+').click()
cy.get('.quantity input[type=number]').should('have.value', 2)
cy.contains('.purchase', 'Add to Cart').click()

cy.contains('nav .carttotal', 2) // the little badge is present
// spy on the Netlify function call
cy.intercept({
  method: 'POST',
  pathname: '/.netlify/functions/create-payment-intent',
}).as('paymentIntent')
cy.contains('nav li', 'Cart').click()

cy.log('**cart page**')
cy.location('pathname').should('equal', '/cart')
// confirm the products in the cart
cy.get('[data-cy=item-in-cart]')
  .should('have.length', 1)
  .first()
  .within(() => {
    cy.contains('.product-size', 'Size: Large')
    cy.contains('[data-cy=item-quantity]', 2)
  })
const ccNumber = '4242424242424242'
const month = '12'
const year = '30'
const cvc = '123'
const zipCode = '90210'
getIframeBody('.stripe-card iframe')
  .find('input[name=cardnumber]')
  .type(`${ccNumber}${month}${year}${cvc}${zipCode}`)
cy.contains('.pay-with-stripe', 'Pay with credit card').click()
// if the payment went through
cy.contains('.success', 'Success!').should('be.visible')

// automatically resets to empty cart
cy.scrollTo('top')
cy.get('nav .carttotal', { timeout: 6000 })
  .should('not.exist') // the little badge is gone
cy.contains('Your cart is empty, fill it up!')
  .should('be.visible')
const getIframeBody = (iframeSelector) =>
  cy.get(iframeSelector)
    .its('0.contentDocument.body')
    .should('not.be.empty')
    .then(cy.wrap)
const ccNumber = '4242424242424242'
const month = '12'
const year = '30'
const cvc = '123'
const zipCode = '90210'
getIframeBody('.stripe-card iframe')
  .find('input[name=cardnumber]')
  .type(`${ccNumber}${month}${year}${cvc}${zipCode}`)
cy.contains('.pay-with-stripe', 'Pay with credit card').click()
// if the payment went through
cy.contains('.success', 'Success!').should('be.visible')

// automatically resets to empty cart
cy.scrollTo('top')
cy.get('nav .carttotal', { timeout: 6000 })
  .should('not.exist') // the little badge is gone
cy.contains('Your cart is empty, fill it up!')
  .should('be.visible')
const ccNumber = '4242424242424242'
const month = '12'
const year = '30'
const cvc = '123'
const zipCode = '90210'
getIframeBody('.stripe-card iframe')
  .find('input[name=cardnumber]')
  .type(`${ccNumber}${month}${year}${cvc}${zipCode}`)
cy.contains('.pay-with-stripe', 'Pay with credit card').click()
// if the payment went through
cy.contains('.success', 'Success!').should('be.visible')

// automatically resets to empty cart
cy.scrollTo('top')
cy.get('nav .carttotal', { timeout: 6000 })
  .should('not.exist') // the little badge is gone
cy.contains('Your cart is empty, fill it up!')
  .should('be.visible')

Deploy To Netlify 🚀

Deploy To Netlify 🚀

Netlify Preview Deploys

How do I demo a new feature before merging?

How do I test the full site before merging?

Netlify Preview Deploys

Netlify Preview Deploys

Netlify Preview Deploys

Need to test it

Netlify Preview Deploys

Need to test it

~/git/cypress-example-netlify-store on feature1
$ npx cypress run --config baseUrl=https://deploy-preview-1--cypress-example-netlify-store.netlify.app/

Let's Run Tests on Netlify!

Netlify Build system

Build plugins

Netlify Build system

  1. Start

  2. Build

  3. Deploy

Netlify Build system

  1. Start

    1. preBuild

  2. Build

    1. postBuild

  3. Deploy

    1. onSuccess

Netlify Build system

  1. Start

    1. preBuild

  2. Build

    1. postBuild

  3. Deploy

    1. onSuccess

Run E2E tests against a local site

Run E2E tests against a deployed site

Netlify Build system

  1. Start

    1. preBuild

  2. Build

    1. postBuild

  3. Deploy

    1. onSuccess

Run E2E tests against a local site

Run E2E tests against a deployed site

(opt-in)

(default)

netlify.toml file

netlify.toml file

[build]
  command = "yarn generate"
  functions = "functions"
  publish = "dist"
  • Be explicit and clear about the build steps 👍
  • Keep this file in source control 👍👍👍

Add Cypress plugin

[build]
  command = "yarn generate"
  functions = "functions"
  publish = "dist"
  
[[plugins]]
  # runs Cypress tests against the deployed URL
  package = "netlify-plugin-cypress"
$ yarn add -D netlify-plugin-cypress

info Direct dependencies
└─ netlify-plugin-cypress@2.1.0

netlify.toml

Control via parameters

[build]
  command = "yarn generate"
  functions = "functions"
  publish = "dist"
  
[[plugins]]
  # runs Cypress tests against the deployed URL
  package = "netlify-plugin-cypress"
  [plugins.inputs]
    record = true
    # you can pass spec, group, tag, etc

Live on the edge 🗡

[build]
  command = "yarn generate"
  functions = "functions"
  publish = "dist"
  
[[plugins]]
  # runs Cypress tests against the deployed URL
  package = "netlify-plugin-cypress"
  [plugins.inputs]
    # do not run the tests after deploy
    enable = false

Let's push a bad change

<li>
  <div class="carttotal" v-if="cartCount > 0">{{ cartCount }}</div>
  <!-- <nuxt-link to="/cart">Cart</nuxt-link> -->
</li>

😳😳😳

We have tests, right

Let's push a bad change

The pull request has no red flags

Let's push a bad change

The deploy went through

Let's push a bad change

  1. The pull request has no red flags

  2. The deploy went through

😳😳😳

We have tests, right

If tests fail, the PR should be red

GitHub Repo

Netlify Build

Cypress Dashboard

Site

Status Checks Flow

  1. Commit triggers Netlify build
  2. Site is deployed
  3. Netlify sets "deploy" status check ✅
  4. Netlify runs E2E tests
  5. Cypress Dashboard records results
  6. Cypress Dashboard sets "tests" status check ✅ or 🔥

If tests fail, the PR should be red

record the tests on Cypress Dashboard

If tests fail, the PR should be red

record the tests on Cypress Dashboard

If tests fail, the PR should be red

set CYPRESS_RECORD_KEY in Netlify build environment

If tests fail, the PR should be red

record the tests on the Dashboard

[[plugins]]
  # runs Cypress tests against the deployed URL
  package = "netlify-plugin-cypress"
  [plugins.inputs]
    record = true

If tests fail, the PR should be red

enable Cypress GitHub integration https://on.cypress.io/gitlab-integration

If tests fail, the PR should be red

pull request is failing!

New Cypress Dashboard Pricing

Do not deploy a broken app

Run tests before deploy

to avoid deploying the broken code

[[plugins]]
  package = "netlify-plugin-cypress"
  # runs Cypress tests against the deployed URL
  [plugins.inputs]
    record = true
  # run Cypress tests before building and deploying
  [plugins.inputs.preBuild]
    enable = true
    # call the same commands as we do locally
    start = 'nuxt start'
    wait-on = 'http://localhost:3000'

Run tests before deploy

Try deploying now

Watch the test run's recording on Cypress Dashboard

[[plugins]]
  package = "netlify-plugin-cypress"
  # runs Cypress tests against the deployed URL
  [plugins.inputs]
    record = true
  # run Cypress tests before building and deploying
  [plugins.inputs.preBuild]
    enable = true
    # full Netlify local environment: 
    # redirects, functions
    start = 'netlify dev'
    wait-on = 'http://localhost:3000'

💡: Run Netlify dev

[[plugins]]
  package = "netlify-plugin-cypress"
  # runs few smoke tests against deployed URL
  [plugins.inputs]
    record = true
    spec = "cypress/integration/smoke/**/*.js"
  # run all tests before building and deploying
  [plugins.inputs.preBuild]
    enable = true
    # full Netlify local environment: 
    # redirects, functions
    start = 'netlify dev'
    wait-on = 'http://localhost:3000'

💡: Smoke tests

AGENDA

  • Tested E-commerce site

  • Deploying to Netlify

  • Testing the deployed site

  • Testing before the deploy

  • Writing Netlify Build plugin

  • Q & A

    • Sli.do event code #cy-netlify

Netlify Build Plugin

Add powerful capabilities to every build.

Want to write a plugin?

(but first, check if there is already a plugin doing what you need!)

module.exports = {
  async onPreBuild ({ constants, utils, inputs }) {
    ...
  },
  
  async onPostBuild ({ constants, utils, inputs }) {
    ...
  },
  
  async onSuccess ({ constants, utils, inputs }) {
    ...
  }
}

Plugin's code

const cypress = require('cypress')
module.exports = {
  async onPreBuild ({ constants, utils, inputs })
  async onPostBuild ({ constants, utils, inputs })
  
  async onSuccess ({ constants, utils, inputs }) {
    const deployPrimeUrl = process.env.DEPLOY_PRIME_URL
    if (!deployPrimeUrl) {
      return utils.build.failPlugin('Missing DEPLOY_PRIME_URL')
    }
    const testResults = await cypress.run({
      config: {
        baseUrl: deployPrimeUrl
      }
    })
  }
}

Plugin's code

name: netlify-plugin-cypress
inputs:
  # by default the Cypress tests run against the deployed URL
  # and these settings apply during the "onSuccess" step
  - name: enable
    description: Run tests against the preview or production deploy
    default: true

  # Cypress comes with built-in Electron browser
  # and this NPM package installs Chromium browser
  - name: browser
    description: Allowed values are chromium, electron
    default: chromium
  ...

Plugin's inputs

manifest.yml

[[plugins]]
  # require the plugin's code from relative folder
  package = "./netlify-plugins/my-plugin"
  [plugins.inputs]
    # my plugin's inputs

💡: Use your local plugin

netlify.toml

Summary

Step 1: Deploy full previews to Netlify

Step 2: Test the site before and after deploy with netlify-plugin-cypress

Gleb Bahmutov

Distinguished Engineer

@bahmutov

Jason Lengstorf

Principal Developer Experience Engineer

@jlengstorf

Thank you 👏

Build, Deploy, and Test

By Cypress.io

Build, Deploy, and Test

In this webcast, we'll show you how to use the Cypress Netlify build plugin to make sure your Jamstack site deploys are bug-free, every time. Video at https://www.youtube.com/watch?v=7kHgOCqWgKE

  • 2,555