From 03ab5367e5b79d4bacbd19c8fdc374bbd36e8e32 Mon Sep 17 00:00:00 2001
From: Simen <simen_opedal@hotmail.com>
Date: Mon, 21 Mar 2022 01:22:23 +0100
Subject: [PATCH] added cypress boundary tests, intergration tests and black
 box tests

---
 frontend/cypress.json                         |    3 +
 frontend/cypress/fixtures/example.json        |    5 +
 .../1-getting-started/todo.spec.js            |  143 ++
 .../2-advanced-examples/actions.spec.js       |  299 ++++
 .../2-advanced-examples/aliasing.spec.js      |   39 +
 .../2-advanced-examples/assertions.spec.js    |  177 +++
 .../2-advanced-examples/connectors.spec.js    |   97 ++
 .../2-advanced-examples/cookies.spec.js       |   77 +
 .../2-advanced-examples/cypress_api.spec.js   |  202 +++
 .../2-advanced-examples/files.spec.js         |   88 ++
 .../2-advanced-examples/local_storage.spec.js |   52 +
 .../2-advanced-examples/location.spec.js      |   32 +
 .../2-advanced-examples/misc.spec.js          |  104 ++
 .../2-advanced-examples/navigation.spec.js    |   56 +
 .../network_requests.spec.js                  |  163 +++
 .../2-advanced-examples/querying.spec.js      |  114 ++
 .../spies_stubs_clocks.spec.js                |  205 +++
 .../2-advanced-examples/traversal.spec.js     |  121 ++
 .../2-advanced-examples/utilities.spec.js     |  110 ++
 .../2-advanced-examples/viewport.spec.js      |   59 +
 .../2-advanced-examples/waiting.spec.js       |   31 +
 .../2-advanced-examples/window.spec.js        |   22 +
 frontend/cypress/integration/FR5_spec.js      |  103 ++
 .../cypress/integration/edit_exercise_spec.js |   76 +
 .../integration/integration_test_spec.js      |  132 ++
 frontend/cypress/integration/register_spec.js |  121 ++
 frontend/cypress/integration/sample_spec.js   |   20 +
 frontend/cypress/plugins/index.js             |   22 +
 frontend/cypress/support/commands.js          |   25 +
 frontend/cypress/support/index.js             |   25 +
 frontend/package-lock.json                    | 1277 +++++++++++++++++
 frontend/package.json                         |    8 +-
 32 files changed, 4005 insertions(+), 3 deletions(-)
 create mode 100644 frontend/cypress.json
 create mode 100644 frontend/cypress/fixtures/example.json
 create mode 100644 frontend/cypress/integration/1-getting-started/todo.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/actions.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/aliasing.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/assertions.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/connectors.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/cookies.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/cypress_api.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/files.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/local_storage.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/location.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/misc.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/navigation.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/network_requests.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/querying.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/spies_stubs_clocks.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/traversal.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/utilities.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/viewport.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/waiting.spec.js
 create mode 100644 frontend/cypress/integration/2-advanced-examples/window.spec.js
 create mode 100644 frontend/cypress/integration/FR5_spec.js
 create mode 100644 frontend/cypress/integration/edit_exercise_spec.js
 create mode 100644 frontend/cypress/integration/integration_test_spec.js
 create mode 100644 frontend/cypress/integration/register_spec.js
 create mode 100644 frontend/cypress/integration/sample_spec.js
 create mode 100644 frontend/cypress/plugins/index.js
 create mode 100644 frontend/cypress/support/commands.js
 create mode 100644 frontend/cypress/support/index.js

diff --git a/frontend/cypress.json b/frontend/cypress.json
new file mode 100644
index 0000000..e7edcf1
--- /dev/null
+++ b/frontend/cypress.json
@@ -0,0 +1,3 @@
+{
+    "baseUrl": "http://localhost:9090"
+}
diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json
new file mode 100644
index 0000000..02e4254
--- /dev/null
+++ b/frontend/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+  "name": "Using fixtures to represent data",
+  "email": "hello@cypress.io",
+  "body": "Fixtures are a great way to mock data for responses to routes"
+}
diff --git a/frontend/cypress/integration/1-getting-started/todo.spec.js b/frontend/cypress/integration/1-getting-started/todo.spec.js
new file mode 100644
index 0000000..4768ff9
--- /dev/null
+++ b/frontend/cypress/integration/1-getting-started/todo.spec.js
@@ -0,0 +1,143 @@
+/// <reference types="cypress" />
+
+// Welcome to Cypress!
+//
+// This spec file contains a variety of sample tests
+// for a todo list app that are designed to demonstrate
+// the power of writing tests in Cypress.
+//
+// To learn more about how Cypress works and
+// what makes it such an awesome testing tool,
+// please read our getting started guide:
+// https://on.cypress.io/introduction-to-cypress
+
+describe('example to-do app', () => {
+  beforeEach(() => {
+    // Cypress starts out with a blank slate for each test
+    // so we must tell it to visit our website with the `cy.visit()` command.
+    // Since we want to visit the same URL at the start of all our tests,
+    // we include it in our beforeEach function so that it runs before each test
+    cy.visit('https://example.cypress.io/todo')
+  })
+
+  it('displays two todo items by default', () => {
+    // We use the `cy.get()` command to get all elements that match the selector.
+    // Then, we use `should` to assert that there are two matched items,
+    // which are the two default items.
+    cy.get('.todo-list li').should('have.length', 2)
+
+    // We can go even further and check that the default todos each contain
+    // the correct text. We use the `first` and `last` functions
+    // to get just the first and last matched elements individually,
+    // and then perform an assertion with `should`.
+    cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
+    cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
+  })
+
+  it('can add new todo items', () => {
+    // We'll store our item text in a variable so we can reuse it
+    const newItem = 'Feed the cat'
+
+    // Let's get the input element and use the `type` command to
+    // input our new list item. After typing the content of our item,
+    // we need to type the enter key as well in order to submit the input.
+    // This input has a data-test attribute so we'll use that to select the
+    // element in accordance with best practices:
+    // https://on.cypress.io/selecting-elements
+    cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
+
+    // Now that we've typed our new item, let's check that it actually was added to the list.
+    // Since it's the newest item, it should exist as the last element in the list.
+    // In addition, with the two default items, we should have a total of 3 elements in the list.
+    // Since assertions yield the element that was asserted on,
+    // we can chain both of these assertions together into a single statement.
+    cy.get('.todo-list li')
+      .should('have.length', 3)
+      .last()
+      .should('have.text', newItem)
+  })
+
+  it('can check off an item as completed', () => {
+    // In addition to using the `get` command to get an element by selector,
+    // we can also use the `contains` command to get an element by its contents.
+    // However, this will yield the <label>, which is lowest-level element that contains the text.
+    // In order to check the item, we'll find the <input> element for this <label>
+    // by traversing up the dom to the parent element. From there, we can `find`
+    // the child checkbox <input> element and use the `check` command to check it.
+    cy.contains('Pay electric bill')
+      .parent()
+      .find('input[type=checkbox]')
+      .check()
+
+    // Now that we've checked the button, we can go ahead and make sure
+    // that the list element is now marked as completed.
+    // Again we'll use `contains` to find the <label> element and then use the `parents` command
+    // to traverse multiple levels up the dom until we find the corresponding <li> element.
+    // Once we get that element, we can assert that it has the completed class.
+    cy.contains('Pay electric bill')
+      .parents('li')
+      .should('have.class', 'completed')
+  })
+
+  context('with a checked task', () => {
+    beforeEach(() => {
+      // We'll take the command we used above to check off an element
+      // Since we want to perform multiple tests that start with checking
+      // one element, we put it in the beforeEach hook
+      // so that it runs at the start of every test.
+      cy.contains('Pay electric bill')
+        .parent()
+        .find('input[type=checkbox]')
+        .check()
+    })
+
+    it('can filter for uncompleted tasks', () => {
+      // We'll click on the "active" button in order to
+      // display only incomplete items
+      cy.contains('Active').click()
+
+      // After filtering, we can assert that there is only the one
+      // incomplete item in the list.
+      cy.get('.todo-list li')
+        .should('have.length', 1)
+        .first()
+        .should('have.text', 'Walk the dog')
+
+      // For good measure, let's also assert that the task we checked off
+      // does not exist on the page.
+      cy.contains('Pay electric bill').should('not.exist')
+    })
+
+    it('can filter for completed tasks', () => {
+      // We can perform similar steps as the test above to ensure
+      // that only completed tasks are shown
+      cy.contains('Completed').click()
+
+      cy.get('.todo-list li')
+        .should('have.length', 1)
+        .first()
+        .should('have.text', 'Pay electric bill')
+
+      cy.contains('Walk the dog').should('not.exist')
+    })
+
+    it('can delete all completed tasks', () => {
+      // First, let's click the "Clear completed" button
+      // `contains` is actually serving two purposes here.
+      // First, it's ensuring that the button exists within the dom.
+      // This button only appears when at least one task is checked
+      // so this command is implicitly verifying that it does exist.
+      // Second, it selects the button so we can click it.
+      cy.contains('Clear completed').click()
+
+      // Then we can make sure that there is only one element
+      // in the list and our element does not exist
+      cy.get('.todo-list li')
+        .should('have.length', 1)
+        .should('not.have.text', 'Pay electric bill')
+
+      // Finally, make sure that the clear button no longer exists.
+      cy.contains('Clear completed').should('not.exist')
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/actions.spec.js b/frontend/cypress/integration/2-advanced-examples/actions.spec.js
new file mode 100644
index 0000000..0926379
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/actions.spec.js
@@ -0,0 +1,299 @@
+/// <reference types="cypress" />
+
+context('Actions', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/actions')
+  })
+
+  // https://on.cypress.io/interacting-with-elements
+
+  it('.type() - type into a DOM element', () => {
+    // https://on.cypress.io/type
+    cy.get('.action-email')
+      .type('fake@email.com').should('have.value', 'fake@email.com')
+
+      // .type() with special character sequences
+      .type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
+      .type('{del}{selectall}{backspace}')
+
+      // .type() with key modifiers
+      .type('{alt}{option}') //these are equivalent
+      .type('{ctrl}{control}') //these are equivalent
+      .type('{meta}{command}{cmd}') //these are equivalent
+      .type('{shift}')
+
+      // Delay each keypress by 0.1 sec
+      .type('slow.typing@email.com', { delay: 100 })
+      .should('have.value', 'slow.typing@email.com')
+
+    cy.get('.action-disabled')
+      // Ignore error checking prior to type
+      // like whether the input is visible or disabled
+      .type('disabled error checking', { force: true })
+      .should('have.value', 'disabled error checking')
+  })
+
+  it('.focus() - focus on a DOM element', () => {
+    // https://on.cypress.io/focus
+    cy.get('.action-focus').focus()
+      .should('have.class', 'focus')
+      .prev().should('have.attr', 'style', 'color: orange;')
+  })
+
+  it('.blur() - blur off a DOM element', () => {
+    // https://on.cypress.io/blur
+    cy.get('.action-blur').type('About to blur').blur()
+      .should('have.class', 'error')
+      .prev().should('have.attr', 'style', 'color: red;')
+  })
+
+  it('.clear() - clears an input or textarea element', () => {
+    // https://on.cypress.io/clear
+    cy.get('.action-clear').type('Clear this text')
+      .should('have.value', 'Clear this text')
+      .clear()
+      .should('have.value', '')
+  })
+
+  it('.submit() - submit a form', () => {
+    // https://on.cypress.io/submit
+    cy.get('.action-form')
+      .find('[type="text"]').type('HALFOFF')
+
+    cy.get('.action-form').submit()
+      .next().should('contain', 'Your form has been submitted!')
+  })
+
+  it('.click() - click on a DOM element', () => {
+    // https://on.cypress.io/click
+    cy.get('.action-btn').click()
+
+    // You can click on 9 specific positions of an element:
+    //  -----------------------------------
+    // | topLeft        top       topRight |
+    // |                                   |
+    // |                                   |
+    // |                                   |
+    // | left          center        right |
+    // |                                   |
+    // |                                   |
+    // |                                   |
+    // | bottomLeft   bottom   bottomRight |
+    //  -----------------------------------
+
+    // clicking in the center of the element is the default
+    cy.get('#action-canvas').click()
+
+    cy.get('#action-canvas').click('topLeft')
+    cy.get('#action-canvas').click('top')
+    cy.get('#action-canvas').click('topRight')
+    cy.get('#action-canvas').click('left')
+    cy.get('#action-canvas').click('right')
+    cy.get('#action-canvas').click('bottomLeft')
+    cy.get('#action-canvas').click('bottom')
+    cy.get('#action-canvas').click('bottomRight')
+
+    // .click() accepts an x and y coordinate
+    // that controls where the click occurs :)
+
+    cy.get('#action-canvas')
+      .click(80, 75) // click 80px on x coord and 75px on y coord
+      .click(170, 75)
+      .click(80, 165)
+      .click(100, 185)
+      .click(125, 190)
+      .click(150, 185)
+      .click(170, 165)
+
+    // click multiple elements by passing multiple: true
+    cy.get('.action-labels>.label').click({ multiple: true })
+
+    // Ignore error checking prior to clicking
+    cy.get('.action-opacity>.btn').click({ force: true })
+  })
+
+  it('.dblclick() - double click on a DOM element', () => {
+    // https://on.cypress.io/dblclick
+
+    // Our app has a listener on 'dblclick' event in our 'scripts.js'
+    // that hides the div and shows an input on double click
+    cy.get('.action-div').dblclick().should('not.be.visible')
+    cy.get('.action-input-hidden').should('be.visible')
+  })
+
+  it('.rightclick() - right click on a DOM element', () => {
+    // https://on.cypress.io/rightclick
+
+    // Our app has a listener on 'contextmenu' event in our 'scripts.js'
+    // that hides the div and shows an input on right click
+    cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
+    cy.get('.rightclick-action-input-hidden').should('be.visible')
+  })
+
+  it('.check() - check a checkbox or radio element', () => {
+    // https://on.cypress.io/check
+
+    // By default, .check() will check all
+    // matching checkbox or radio elements in succession, one after another
+    cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
+      .check().should('be.checked')
+
+    cy.get('.action-radios [type="radio"]').not('[disabled]')
+      .check().should('be.checked')
+
+    // .check() accepts a value argument
+    cy.get('.action-radios [type="radio"]')
+      .check('radio1').should('be.checked')
+
+    // .check() accepts an array of values
+    cy.get('.action-multiple-checkboxes [type="checkbox"]')
+      .check(['checkbox1', 'checkbox2']).should('be.checked')
+
+    // Ignore error checking prior to checking
+    cy.get('.action-checkboxes [disabled]')
+      .check({ force: true }).should('be.checked')
+
+    cy.get('.action-radios [type="radio"]')
+      .check('radio3', { force: true }).should('be.checked')
+  })
+
+  it('.uncheck() - uncheck a checkbox element', () => {
+    // https://on.cypress.io/uncheck
+
+    // By default, .uncheck() will uncheck all matching
+    // checkbox elements in succession, one after another
+    cy.get('.action-check [type="checkbox"]')
+      .not('[disabled]')
+      .uncheck().should('not.be.checked')
+
+    // .uncheck() accepts a value argument
+    cy.get('.action-check [type="checkbox"]')
+      .check('checkbox1')
+      .uncheck('checkbox1').should('not.be.checked')
+
+    // .uncheck() accepts an array of values
+    cy.get('.action-check [type="checkbox"]')
+      .check(['checkbox1', 'checkbox3'])
+      .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
+
+    // Ignore error checking prior to unchecking
+    cy.get('.action-check [disabled]')
+      .uncheck({ force: true }).should('not.be.checked')
+  })
+
+  it('.select() - select an option in a <select> element', () => {
+    // https://on.cypress.io/select
+
+    // at first, no option should be selected
+    cy.get('.action-select')
+      .should('have.value', '--Select a fruit--')
+
+    // Select option(s) with matching text content
+    cy.get('.action-select').select('apples')
+    // confirm the apples were selected
+    // note that each value starts with "fr-" in our HTML
+    cy.get('.action-select').should('have.value', 'fr-apples')
+
+    cy.get('.action-select-multiple')
+      .select(['apples', 'oranges', 'bananas'])
+      // when getting multiple values, invoke "val" method first
+      .invoke('val')
+      .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
+
+    // Select option(s) with matching value
+    cy.get('.action-select').select('fr-bananas')
+      // can attach an assertion right away to the element
+      .should('have.value', 'fr-bananas')
+
+    cy.get('.action-select-multiple')
+      .select(['fr-apples', 'fr-oranges', 'fr-bananas'])
+      .invoke('val')
+      .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
+
+    // assert the selected values include oranges
+    cy.get('.action-select-multiple')
+      .invoke('val').should('include', 'fr-oranges')
+  })
+
+  it('.scrollIntoView() - scroll an element into view', () => {
+    // https://on.cypress.io/scrollintoview
+
+    // normally all of these buttons are hidden,
+    // because they're not within
+    // the viewable area of their parent
+    // (we need to scroll to see them)
+    cy.get('#scroll-horizontal button')
+      .should('not.be.visible')
+
+    // scroll the button into view, as if the user had scrolled
+    cy.get('#scroll-horizontal button').scrollIntoView()
+      .should('be.visible')
+
+    cy.get('#scroll-vertical button')
+      .should('not.be.visible')
+
+    // Cypress handles the scroll direction needed
+    cy.get('#scroll-vertical button').scrollIntoView()
+      .should('be.visible')
+
+    cy.get('#scroll-both button')
+      .should('not.be.visible')
+
+    // Cypress knows to scroll to the right and down
+    cy.get('#scroll-both button').scrollIntoView()
+      .should('be.visible')
+  })
+
+  it('.trigger() - trigger an event on a DOM element', () => {
+    // https://on.cypress.io/trigger
+
+    // To interact with a range input (slider)
+    // we need to set its value & trigger the
+    // event to signal it changed
+
+    // Here, we invoke jQuery's val() method to set
+    // the value and trigger the 'change' event
+    cy.get('.trigger-input-range')
+      .invoke('val', 25)
+      .trigger('change')
+      .get('input[type=range]').siblings('p')
+      .should('have.text', '25')
+  })
+
+  it('cy.scrollTo() - scroll the window or element to a position', () => {
+    // https://on.cypress.io/scrollto
+
+    // You can scroll to 9 specific positions of an element:
+    //  -----------------------------------
+    // | topLeft        top       topRight |
+    // |                                   |
+    // |                                   |
+    // |                                   |
+    // | left          center        right |
+    // |                                   |
+    // |                                   |
+    // |                                   |
+    // | bottomLeft   bottom   bottomRight |
+    //  -----------------------------------
+
+    // if you chain .scrollTo() off of cy, we will
+    // scroll the entire window
+    cy.scrollTo('bottom')
+
+    cy.get('#scrollable-horizontal').scrollTo('right')
+
+    // or you can scroll to a specific coordinate:
+    // (x axis, y axis) in pixels
+    cy.get('#scrollable-vertical').scrollTo(250, 250)
+
+    // or you can scroll to a specific percentage
+    // of the (width, height) of the element
+    cy.get('#scrollable-both').scrollTo('75%', '25%')
+
+    // control the easing of the scroll (default is 'swing')
+    cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
+
+    // control the duration of the scroll (in ms)
+    cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/aliasing.spec.js b/frontend/cypress/integration/2-advanced-examples/aliasing.spec.js
new file mode 100644
index 0000000..a02fb2b
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/aliasing.spec.js
@@ -0,0 +1,39 @@
+/// <reference types="cypress" />
+
+context('Aliasing', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/aliasing')
+  })
+
+  it('.as() - alias a DOM element for later use', () => {
+    // https://on.cypress.io/as
+
+    // Alias a DOM element for use later
+    // We don't have to traverse to the element
+    // later in our code, we reference it with @
+
+    cy.get('.as-table').find('tbody>tr')
+      .first().find('td').first()
+      .find('button').as('firstBtn')
+
+    // when we reference the alias, we place an
+    // @ in front of its name
+    cy.get('@firstBtn').click()
+
+    cy.get('@firstBtn')
+      .should('have.class', 'btn-success')
+      .and('contain', 'Changed')
+  })
+
+  it('.as() - alias a route for later use', () => {
+    // Alias the route to wait for its response
+    cy.intercept('GET', '**/comments/*').as('getComment')
+
+    // we have code that gets a comment when
+    // the button is clicked in scripts.js
+    cy.get('.network-btn').click()
+
+    // https://on.cypress.io/wait
+    cy.wait('@getComment').its('response.statusCode').should('eq', 200)
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/assertions.spec.js b/frontend/cypress/integration/2-advanced-examples/assertions.spec.js
new file mode 100644
index 0000000..5ba93d1
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/assertions.spec.js
@@ -0,0 +1,177 @@
+/// <reference types="cypress" />
+
+context('Assertions', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/assertions')
+  })
+
+  describe('Implicit Assertions', () => {
+    it('.should() - make an assertion about the current subject', () => {
+      // https://on.cypress.io/should
+      cy.get('.assertion-table')
+        .find('tbody tr:last')
+        .should('have.class', 'success')
+        .find('td')
+        .first()
+        // checking the text of the <td> element in various ways
+        .should('have.text', 'Column content')
+        .should('contain', 'Column content')
+        .should('have.html', 'Column content')
+        // chai-jquery uses "is()" to check if element matches selector
+        .should('match', 'td')
+        // to match text content against a regular expression
+        // first need to invoke jQuery method text()
+        // and then match using regular expression
+        .invoke('text')
+        .should('match', /column content/i)
+
+      // a better way to check element's text content against a regular expression
+      // is to use "cy.contains"
+      // https://on.cypress.io/contains
+      cy.get('.assertion-table')
+        .find('tbody tr:last')
+        // finds first <td> element with text content matching regular expression
+        .contains('td', /column content/i)
+        .should('be.visible')
+
+      // for more information about asserting element's text
+      // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
+    })
+
+    it('.and() - chain multiple assertions together', () => {
+      // https://on.cypress.io/and
+      cy.get('.assertions-link')
+        .should('have.class', 'active')
+        .and('have.attr', 'href')
+        .and('include', 'cypress.io')
+    })
+  })
+
+  describe('Explicit Assertions', () => {
+    // https://on.cypress.io/assertions
+    it('expect - make an assertion about a specified subject', () => {
+      // We can use Chai's BDD style assertions
+      expect(true).to.be.true
+      const o = { foo: 'bar' }
+
+      expect(o).to.equal(o)
+      expect(o).to.deep.equal({ foo: 'bar' })
+      // matching text using regular expression
+      expect('FooBar').to.match(/bar$/i)
+    })
+
+    it('pass your own callback function to should()', () => {
+      // Pass a function to should that can have any number
+      // of explicit assertions within it.
+      // The ".should(cb)" function will be retried
+      // automatically until it passes all your explicit assertions or times out.
+      cy.get('.assertions-p')
+        .find('p')
+        .should(($p) => {
+          // https://on.cypress.io/$
+          // return an array of texts from all of the p's
+          // @ts-ignore TS6133 unused variable
+          const texts = $p.map((i, el) => Cypress.$(el).text())
+
+          // jquery map returns jquery object
+          // and .get() convert this to simple array
+          const paragraphs = texts.get()
+
+          // array should have length of 3
+          expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
+
+          // use second argument to expect(...) to provide clear
+          // message with each assertion
+          expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
+            'Some text from first p',
+            'More text from second p',
+            'And even more text from third p',
+          ])
+        })
+    })
+
+    it('finds element by class name regex', () => {
+      cy.get('.docs-header')
+        .find('div')
+        // .should(cb) callback function will be retried
+        .should(($div) => {
+          expect($div).to.have.length(1)
+
+          const className = $div[0].className
+
+          expect(className).to.match(/heading-/)
+        })
+        // .then(cb) callback is not retried,
+        // it either passes or fails
+        .then(($div) => {
+          expect($div, 'text content').to.have.text('Introduction')
+        })
+    })
+
+    it('can throw any error', () => {
+      cy.get('.docs-header')
+        .find('div')
+        .should(($div) => {
+          if ($div.length !== 1) {
+            // you can throw your own errors
+            throw new Error('Did not find 1 element')
+          }
+
+          const className = $div[0].className
+
+          if (!className.match(/heading-/)) {
+            throw new Error(`Could not find class "heading-" in ${className}`)
+          }
+        })
+    })
+
+    it('matches unknown text between two elements', () => {
+      /**
+       * Text from the first element.
+       * @type {string}
+      */
+      let text
+
+      /**
+       * Normalizes passed text,
+       * useful before comparing text with spaces and different capitalization.
+       * @param {string} s Text to normalize
+      */
+      const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
+
+      cy.get('.two-elements')
+        .find('.first')
+        .then(($first) => {
+          // save text from the first element
+          text = normalizeText($first.text())
+        })
+
+      cy.get('.two-elements')
+        .find('.second')
+        .should(($div) => {
+          // we can massage text before comparing
+          const secondText = normalizeText($div.text())
+
+          expect(secondText, 'second text').to.equal(text)
+        })
+    })
+
+    it('assert - assert shape of an object', () => {
+      const person = {
+        name: 'Joe',
+        age: 20,
+      }
+
+      assert.isObject(person, 'value is object')
+    })
+
+    it('retries the should callback until assertions pass', () => {
+      cy.get('#random-number')
+        .should(($div) => {
+          const n = parseFloat($div.text())
+
+          expect(n).to.be.gte(1).and.be.lte(10)
+        })
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/connectors.spec.js b/frontend/cypress/integration/2-advanced-examples/connectors.spec.js
new file mode 100644
index 0000000..ae87991
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/connectors.spec.js
@@ -0,0 +1,97 @@
+/// <reference types="cypress" />
+
+context('Connectors', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/connectors')
+  })
+
+  it('.each() - iterate over an array of elements', () => {
+    // https://on.cypress.io/each
+    cy.get('.connectors-each-ul>li')
+      .each(($el, index, $list) => {
+        console.log($el, index, $list)
+      })
+  })
+
+  it('.its() - get properties on the current subject', () => {
+    // https://on.cypress.io/its
+    cy.get('.connectors-its-ul>li')
+      // calls the 'length' property yielding that value
+      .its('length')
+      .should('be.gt', 2)
+  })
+
+  it('.invoke() - invoke a function on the current subject', () => {
+    // our div is hidden in our script.js
+    // $('.connectors-div').hide()
+
+    // https://on.cypress.io/invoke
+    cy.get('.connectors-div').should('be.hidden')
+      // call the jquery method 'show' on the 'div.container'
+      .invoke('show')
+      .should('be.visible')
+  })
+
+  it('.spread() - spread an array as individual args to callback function', () => {
+    // https://on.cypress.io/spread
+    const arr = ['foo', 'bar', 'baz']
+
+    cy.wrap(arr).spread((foo, bar, baz) => {
+      expect(foo).to.eq('foo')
+      expect(bar).to.eq('bar')
+      expect(baz).to.eq('baz')
+    })
+  })
+
+  describe('.then()', () => {
+    it('invokes a callback function with the current subject', () => {
+      // https://on.cypress.io/then
+      cy.get('.connectors-list > li')
+        .then(($lis) => {
+          expect($lis, '3 items').to.have.length(3)
+          expect($lis.eq(0), 'first item').to.contain('Walk the dog')
+          expect($lis.eq(1), 'second item').to.contain('Feed the cat')
+          expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
+        })
+    })
+
+    it('yields the returned value to the next command', () => {
+      cy.wrap(1)
+        .then((num) => {
+          expect(num).to.equal(1)
+
+          return 2
+        })
+        .then((num) => {
+          expect(num).to.equal(2)
+        })
+    })
+
+    it('yields the original subject without return', () => {
+      cy.wrap(1)
+        .then((num) => {
+          expect(num).to.equal(1)
+          // note that nothing is returned from this callback
+        })
+        .then((num) => {
+          // this callback receives the original unchanged value 1
+          expect(num).to.equal(1)
+        })
+    })
+
+    it('yields the value yielded by the last Cypress command inside', () => {
+      cy.wrap(1)
+        .then((num) => {
+          expect(num).to.equal(1)
+          // note how we run a Cypress command
+          // the result yielded by this Cypress command
+          // will be passed to the second ".then"
+          cy.wrap(2)
+        })
+        .then((num) => {
+          // this callback receives the value yielded by "cy.wrap(2)"
+          expect(num).to.equal(2)
+        })
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/cookies.spec.js b/frontend/cypress/integration/2-advanced-examples/cookies.spec.js
new file mode 100644
index 0000000..31587ff
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/cookies.spec.js
@@ -0,0 +1,77 @@
+/// <reference types="cypress" />
+
+context('Cookies', () => {
+  beforeEach(() => {
+    Cypress.Cookies.debug(true)
+
+    cy.visit('https://example.cypress.io/commands/cookies')
+
+    // clear cookies again after visiting to remove
+    // any 3rd party cookies picked up such as cloudflare
+    cy.clearCookies()
+  })
+
+  it('cy.getCookie() - get a browser cookie', () => {
+    // https://on.cypress.io/getcookie
+    cy.get('#getCookie .set-a-cookie').click()
+
+    // cy.getCookie() yields a cookie object
+    cy.getCookie('token').should('have.property', 'value', '123ABC')
+  })
+
+  it('cy.getCookies() - get browser cookies', () => {
+    // https://on.cypress.io/getcookies
+    cy.getCookies().should('be.empty')
+
+    cy.get('#getCookies .set-a-cookie').click()
+
+    // cy.getCookies() yields an array of cookies
+    cy.getCookies().should('have.length', 1).should((cookies) => {
+      // each cookie has these properties
+      expect(cookies[0]).to.have.property('name', 'token')
+      expect(cookies[0]).to.have.property('value', '123ABC')
+      expect(cookies[0]).to.have.property('httpOnly', false)
+      expect(cookies[0]).to.have.property('secure', false)
+      expect(cookies[0]).to.have.property('domain')
+      expect(cookies[0]).to.have.property('path')
+    })
+  })
+
+  it('cy.setCookie() - set a browser cookie', () => {
+    // https://on.cypress.io/setcookie
+    cy.getCookies().should('be.empty')
+
+    cy.setCookie('foo', 'bar')
+
+    // cy.getCookie() yields a cookie object
+    cy.getCookie('foo').should('have.property', 'value', 'bar')
+  })
+
+  it('cy.clearCookie() - clear a browser cookie', () => {
+    // https://on.cypress.io/clearcookie
+    cy.getCookie('token').should('be.null')
+
+    cy.get('#clearCookie .set-a-cookie').click()
+
+    cy.getCookie('token').should('have.property', 'value', '123ABC')
+
+    // cy.clearCookies() yields null
+    cy.clearCookie('token').should('be.null')
+
+    cy.getCookie('token').should('be.null')
+  })
+
+  it('cy.clearCookies() - clear browser cookies', () => {
+    // https://on.cypress.io/clearcookies
+    cy.getCookies().should('be.empty')
+
+    cy.get('#clearCookies .set-a-cookie').click()
+
+    cy.getCookies().should('have.length', 1)
+
+    // cy.clearCookies() yields null
+    cy.clearCookies()
+
+    cy.getCookies().should('be.empty')
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/cypress_api.spec.js b/frontend/cypress/integration/2-advanced-examples/cypress_api.spec.js
new file mode 100644
index 0000000..ec8ceae
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/cypress_api.spec.js
@@ -0,0 +1,202 @@
+/// <reference types="cypress" />
+
+context('Cypress.Commands', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  // https://on.cypress.io/custom-commands
+
+  it('.add() - create a custom command', () => {
+    Cypress.Commands.add('console', {
+      prevSubject: true,
+    }, (subject, method) => {
+      // the previous subject is automatically received
+      // and the commands arguments are shifted
+
+      // allow us to change the console method used
+      method = method || 'log'
+
+      // log the subject to the console
+      // @ts-ignore TS7017
+      console[method]('The subject is', subject)
+
+      // whatever we return becomes the new subject
+      // we don't want to change the subject so
+      // we return whatever was passed in
+      return subject
+    })
+
+    // @ts-ignore TS2339
+    cy.get('button').console('info').then(($button) => {
+      // subject is still $button
+    })
+  })
+})
+
+context('Cypress.Cookies', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  // https://on.cypress.io/cookies
+  it('.debug() - enable or disable debugging', () => {
+    Cypress.Cookies.debug(true)
+
+    // Cypress will now log in the console when
+    // cookies are set or cleared
+    cy.setCookie('fakeCookie', '123ABC')
+    cy.clearCookie('fakeCookie')
+    cy.setCookie('fakeCookie', '123ABC')
+    cy.clearCookie('fakeCookie')
+    cy.setCookie('fakeCookie', '123ABC')
+  })
+
+  it('.preserveOnce() - preserve cookies by key', () => {
+    // normally cookies are reset after each test
+    cy.getCookie('fakeCookie').should('not.be.ok')
+
+    // preserving a cookie will not clear it when
+    // the next test starts
+    cy.setCookie('lastCookie', '789XYZ')
+    Cypress.Cookies.preserveOnce('lastCookie')
+  })
+
+  it('.defaults() - set defaults for all cookies', () => {
+    // now any cookie with the name 'session_id' will
+    // not be cleared before each new test runs
+    Cypress.Cookies.defaults({
+      preserve: 'session_id',
+    })
+  })
+})
+
+context('Cypress.arch', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Get CPU architecture name of underlying OS', () => {
+    // https://on.cypress.io/arch
+    expect(Cypress.arch).to.exist
+  })
+})
+
+context('Cypress.config()', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Get and set configuration options', () => {
+    // https://on.cypress.io/config
+    let myConfig = Cypress.config()
+
+    expect(myConfig).to.have.property('animationDistanceThreshold', 5)
+    expect(myConfig).to.have.property('baseUrl', null)
+    expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
+    expect(myConfig).to.have.property('requestTimeout', 5000)
+    expect(myConfig).to.have.property('responseTimeout', 30000)
+    expect(myConfig).to.have.property('viewportHeight', 660)
+    expect(myConfig).to.have.property('viewportWidth', 1000)
+    expect(myConfig).to.have.property('pageLoadTimeout', 60000)
+    expect(myConfig).to.have.property('waitForAnimations', true)
+
+    expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
+
+    // this will change the config for the rest of your tests!
+    Cypress.config('pageLoadTimeout', 20000)
+
+    expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
+
+    Cypress.config('pageLoadTimeout', 60000)
+  })
+})
+
+context('Cypress.dom', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  // https://on.cypress.io/dom
+  it('.isHidden() - determine if a DOM element is hidden', () => {
+    let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
+    let visibleP = Cypress.$('.dom-p p.visible').get(0)
+
+    // our first paragraph has css class 'hidden'
+    expect(Cypress.dom.isHidden(hiddenP)).to.be.true
+    expect(Cypress.dom.isHidden(visibleP)).to.be.false
+  })
+})
+
+context('Cypress.env()', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  // We can set environment variables for highly dynamic values
+
+  // https://on.cypress.io/environment-variables
+  it('Get environment variables', () => {
+    // https://on.cypress.io/env
+    // set multiple environment variables
+    Cypress.env({
+      host: 'veronica.dev.local',
+      api_server: 'http://localhost:8888/v1/',
+    })
+
+    // get environment variable
+    expect(Cypress.env('host')).to.eq('veronica.dev.local')
+
+    // set environment variable
+    Cypress.env('api_server', 'http://localhost:8888/v2/')
+    expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
+
+    // get all environment variable
+    expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
+    expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
+  })
+})
+
+context('Cypress.log', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Control what is printed to the Command Log', () => {
+    // https://on.cypress.io/cypress-log
+  })
+})
+
+context('Cypress.platform', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Get underlying OS name', () => {
+    // https://on.cypress.io/platform
+    expect(Cypress.platform).to.be.exist
+  })
+})
+
+context('Cypress.version', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Get current version of Cypress being run', () => {
+    // https://on.cypress.io/version
+    expect(Cypress.version).to.be.exist
+  })
+})
+
+context('Cypress.spec', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/cypress-api')
+  })
+
+  it('Get current spec information', () => {
+    // https://on.cypress.io/spec
+    // wrap the object so we can inspect it easily by clicking in the command log
+    cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/files.spec.js b/frontend/cypress/integration/2-advanced-examples/files.spec.js
new file mode 100644
index 0000000..b827343
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/files.spec.js
@@ -0,0 +1,88 @@
+/// <reference types="cypress" />
+
+/// JSON fixture file can be loaded directly using
+// the built-in JavaScript bundler
+// @ts-ignore
+const requiredExample = require('../../fixtures/example')
+
+context('Files', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/files')
+  })
+
+  beforeEach(() => {
+    // load example.json fixture file and store
+    // in the test context object
+    cy.fixture('example.json').as('example')
+  })
+
+  it('cy.fixture() - load a fixture', () => {
+    // https://on.cypress.io/fixture
+
+    // Instead of writing a response inline you can
+    // use a fixture file's content.
+
+    // when application makes an Ajax request matching "GET **/comments/*"
+    // Cypress will intercept it and reply with the object in `example.json` fixture
+    cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
+
+    // we have code that gets a comment when
+    // the button is clicked in scripts.js
+    cy.get('.fixture-btn').click()
+
+    cy.wait('@getComment').its('response.body')
+      .should('have.property', 'name')
+      .and('include', 'Using fixtures to represent data')
+  })
+
+  it('cy.fixture() or require - load a fixture', function () {
+    // we are inside the "function () { ... }"
+    // callback and can use test context object "this"
+    // "this.example" was loaded in "beforeEach" function callback
+    expect(this.example, 'fixture in the test context')
+      .to.deep.equal(requiredExample)
+
+    // or use "cy.wrap" and "should('deep.equal', ...)" assertion
+    cy.wrap(this.example)
+      .should('deep.equal', requiredExample)
+  })
+
+  it('cy.readFile() - read file contents', () => {
+    // https://on.cypress.io/readfile
+
+    // You can read a file and yield its contents
+    // The filePath is relative to your project's root.
+    cy.readFile('cypress.json').then((json) => {
+      expect(json).to.be.an('object')
+    })
+  })
+
+  it('cy.writeFile() - write to a file', () => {
+    // https://on.cypress.io/writefile
+
+    // You can write to a file
+
+    // Use a response from a request to automatically
+    // generate a fixture file for use later
+    cy.request('https://jsonplaceholder.cypress.io/users')
+      .then((response) => {
+        cy.writeFile('cypress/fixtures/users.json', response.body)
+      })
+
+    cy.fixture('users').should((users) => {
+      expect(users[0].name).to.exist
+    })
+
+    // JavaScript arrays and objects are stringified
+    // and formatted into text.
+    cy.writeFile('cypress/fixtures/profile.json', {
+      id: 8739,
+      name: 'Jane',
+      email: 'jane@example.com',
+    })
+
+    cy.fixture('profile').should((profile) => {
+      expect(profile.name).to.eq('Jane')
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/local_storage.spec.js b/frontend/cypress/integration/2-advanced-examples/local_storage.spec.js
new file mode 100644
index 0000000..534d8bd
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/local_storage.spec.js
@@ -0,0 +1,52 @@
+/// <reference types="cypress" />
+
+context('Local Storage', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/local-storage')
+  })
+  // Although local storage is automatically cleared
+  // in between tests to maintain a clean state
+  // sometimes we need to clear the local storage manually
+
+  it('cy.clearLocalStorage() - clear all data in local storage', () => {
+    // https://on.cypress.io/clearlocalstorage
+    cy.get('.ls-btn').click().should(() => {
+      expect(localStorage.getItem('prop1')).to.eq('red')
+      expect(localStorage.getItem('prop2')).to.eq('blue')
+      expect(localStorage.getItem('prop3')).to.eq('magenta')
+    })
+
+    // clearLocalStorage() yields the localStorage object
+    cy.clearLocalStorage().should((ls) => {
+      expect(ls.getItem('prop1')).to.be.null
+      expect(ls.getItem('prop2')).to.be.null
+      expect(ls.getItem('prop3')).to.be.null
+    })
+
+    cy.get('.ls-btn').click().should(() => {
+      expect(localStorage.getItem('prop1')).to.eq('red')
+      expect(localStorage.getItem('prop2')).to.eq('blue')
+      expect(localStorage.getItem('prop3')).to.eq('magenta')
+    })
+
+    // Clear key matching string in Local Storage
+    cy.clearLocalStorage('prop1').should((ls) => {
+      expect(ls.getItem('prop1')).to.be.null
+      expect(ls.getItem('prop2')).to.eq('blue')
+      expect(ls.getItem('prop3')).to.eq('magenta')
+    })
+
+    cy.get('.ls-btn').click().should(() => {
+      expect(localStorage.getItem('prop1')).to.eq('red')
+      expect(localStorage.getItem('prop2')).to.eq('blue')
+      expect(localStorage.getItem('prop3')).to.eq('magenta')
+    })
+
+    // Clear keys matching regex in Local Storage
+    cy.clearLocalStorage(/prop1|2/).should((ls) => {
+      expect(ls.getItem('prop1')).to.be.null
+      expect(ls.getItem('prop2')).to.be.null
+      expect(ls.getItem('prop3')).to.eq('magenta')
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/location.spec.js b/frontend/cypress/integration/2-advanced-examples/location.spec.js
new file mode 100644
index 0000000..299867d
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/location.spec.js
@@ -0,0 +1,32 @@
+/// <reference types="cypress" />
+
+context('Location', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/location')
+  })
+
+  it('cy.hash() - get the current URL hash', () => {
+    // https://on.cypress.io/hash
+    cy.hash().should('be.empty')
+  })
+
+  it('cy.location() - get window.location', () => {
+    // https://on.cypress.io/location
+    cy.location().should((location) => {
+      expect(location.hash).to.be.empty
+      expect(location.href).to.eq('https://example.cypress.io/commands/location')
+      expect(location.host).to.eq('example.cypress.io')
+      expect(location.hostname).to.eq('example.cypress.io')
+      expect(location.origin).to.eq('https://example.cypress.io')
+      expect(location.pathname).to.eq('/commands/location')
+      expect(location.port).to.eq('')
+      expect(location.protocol).to.eq('https:')
+      expect(location.search).to.be.empty
+    })
+  })
+
+  it('cy.url() - get the current URL', () => {
+    // https://on.cypress.io/url
+    cy.url().should('eq', 'https://example.cypress.io/commands/location')
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/misc.spec.js b/frontend/cypress/integration/2-advanced-examples/misc.spec.js
new file mode 100644
index 0000000..7222bf4
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/misc.spec.js
@@ -0,0 +1,104 @@
+/// <reference types="cypress" />
+
+context('Misc', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/misc')
+  })
+
+  it('.end() - end the command chain', () => {
+    // https://on.cypress.io/end
+
+    // cy.end is useful when you want to end a chain of commands
+    // and force Cypress to re-query from the root element
+    cy.get('.misc-table').within(() => {
+      // ends the current chain and yields null
+      cy.contains('Cheryl').click().end()
+
+      // queries the entire table again
+      cy.contains('Charles').click()
+    })
+  })
+
+  it('cy.exec() - execute a system command', () => {
+    // execute a system command.
+    // so you can take actions necessary for
+    // your test outside the scope of Cypress.
+    // https://on.cypress.io/exec
+
+    // we can use Cypress.platform string to
+    // select appropriate command
+    // https://on.cypress/io/platform
+    cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
+
+    // on CircleCI Windows build machines we have a failure to run bash shell
+    // https://github.com/cypress-io/cypress/issues/5169
+    // so skip some of the tests by passing flag "--env circle=true"
+    const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
+
+    if (isCircleOnWindows) {
+      cy.log('Skipping test on CircleCI')
+
+      return
+    }
+
+    // cy.exec problem on Shippable CI
+    // https://github.com/cypress-io/cypress/issues/6718
+    const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
+
+    if (isShippable) {
+      cy.log('Skipping test on ShippableCI')
+
+      return
+    }
+
+    cy.exec('echo Jane Lane')
+      .its('stdout').should('contain', 'Jane Lane')
+
+    if (Cypress.platform === 'win32') {
+      cy.exec('print cypress.json')
+        .its('stderr').should('be.empty')
+    } else {
+      cy.exec('cat cypress.json')
+        .its('stderr').should('be.empty')
+
+      cy.exec('pwd')
+        .its('code').should('eq', 0)
+    }
+  })
+
+  it('cy.focused() - get the DOM element that has focus', () => {
+    // https://on.cypress.io/focused
+    cy.get('.misc-form').find('#name').click()
+    cy.focused().should('have.id', 'name')
+
+    cy.get('.misc-form').find('#description').click()
+    cy.focused().should('have.id', 'description')
+  })
+
+  context('Cypress.Screenshot', function () {
+    it('cy.screenshot() - take a screenshot', () => {
+      // https://on.cypress.io/screenshot
+      cy.screenshot('my-image')
+    })
+
+    it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
+      Cypress.Screenshot.defaults({
+        blackout: ['.foo'],
+        capture: 'viewport',
+        clip: { x: 0, y: 0, width: 200, height: 200 },
+        scale: false,
+        disableTimersAndAnimations: true,
+        screenshotOnRunFailure: true,
+        onBeforeScreenshot () { },
+        onAfterScreenshot () { },
+      })
+    })
+  })
+
+  it('cy.wrap() - wrap an object', () => {
+    // https://on.cypress.io/wrap
+    cy.wrap({ foo: 'bar' })
+      .should('have.property', 'foo')
+      .and('include', 'bar')
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/navigation.spec.js b/frontend/cypress/integration/2-advanced-examples/navigation.spec.js
new file mode 100644
index 0000000..b85a468
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/navigation.spec.js
@@ -0,0 +1,56 @@
+/// <reference types="cypress" />
+
+context('Navigation', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io')
+    cy.get('.navbar-nav').contains('Commands').click()
+    cy.get('.dropdown-menu').contains('Navigation').click()
+  })
+
+  it('cy.go() - go back or forward in the browser\'s history', () => {
+    // https://on.cypress.io/go
+
+    cy.location('pathname').should('include', 'navigation')
+
+    cy.go('back')
+    cy.location('pathname').should('not.include', 'navigation')
+
+    cy.go('forward')
+    cy.location('pathname').should('include', 'navigation')
+
+    // clicking back
+    cy.go(-1)
+    cy.location('pathname').should('not.include', 'navigation')
+
+    // clicking forward
+    cy.go(1)
+    cy.location('pathname').should('include', 'navigation')
+  })
+
+  it('cy.reload() - reload the page', () => {
+    // https://on.cypress.io/reload
+    cy.reload()
+
+    // reload the page without using the cache
+    cy.reload(true)
+  })
+
+  it('cy.visit() - visit a remote url', () => {
+    // https://on.cypress.io/visit
+
+    // Visit any sub-domain of your current domain
+
+    // Pass options to the visit
+    cy.visit('https://example.cypress.io/commands/navigation', {
+      timeout: 50000, // increase total time for the visit to resolve
+      onBeforeLoad (contentWindow) {
+        // contentWindow is the remote page's window object
+        expect(typeof contentWindow === 'object').to.be.true
+      },
+      onLoad (contentWindow) {
+        // contentWindow is the remote page's window object
+        expect(typeof contentWindow === 'object').to.be.true
+      },
+    })
+    })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/network_requests.spec.js b/frontend/cypress/integration/2-advanced-examples/network_requests.spec.js
new file mode 100644
index 0000000..11213a0
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/network_requests.spec.js
@@ -0,0 +1,163 @@
+/// <reference types="cypress" />
+
+context('Network Requests', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/network-requests')
+  })
+
+  // Manage HTTP requests in your app
+
+  it('cy.request() - make an XHR request', () => {
+    // https://on.cypress.io/request
+    cy.request('https://jsonplaceholder.cypress.io/comments')
+      .should((response) => {
+        expect(response.status).to.eq(200)
+        // the server sometimes gets an extra comment posted from another machine
+        // which gets returned as 1 extra object
+        expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
+        expect(response).to.have.property('headers')
+        expect(response).to.have.property('duration')
+      })
+  })
+
+  it('cy.request() - verify response using BDD syntax', () => {
+    cy.request('https://jsonplaceholder.cypress.io/comments')
+    .then((response) => {
+      // https://on.cypress.io/assertions
+      expect(response).property('status').to.equal(200)
+      expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
+      expect(response).to.include.keys('headers', 'duration')
+    })
+  })
+
+  it('cy.request() with query parameters', () => {
+    // will execute request
+    // https://jsonplaceholder.cypress.io/comments?postId=1&id=3
+    cy.request({
+      url: 'https://jsonplaceholder.cypress.io/comments',
+      qs: {
+        postId: 1,
+        id: 3,
+      },
+    })
+    .its('body')
+    .should('be.an', 'array')
+    .and('have.length', 1)
+    .its('0') // yields first element of the array
+    .should('contain', {
+      postId: 1,
+      id: 3,
+    })
+  })
+
+  it('cy.request() - pass result to the second request', () => {
+    // first, let's find out the userId of the first user we have
+    cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
+      .its('body') // yields the response object
+      .its('0') // yields the first element of the returned list
+      // the above two commands its('body').its('0')
+      // can be written as its('body.0')
+      // if you do not care about TypeScript checks
+      .then((user) => {
+        expect(user).property('id').to.be.a('number')
+        // make a new post on behalf of the user
+        cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
+          userId: user.id,
+          title: 'Cypress Test Runner',
+          body: 'Fast, easy and reliable testing for anything that runs in a browser.',
+        })
+      })
+      // note that the value here is the returned value of the 2nd request
+      // which is the new post object
+      .then((response) => {
+        expect(response).property('status').to.equal(201) // new entity created
+        expect(response).property('body').to.contain({
+          title: 'Cypress Test Runner',
+        })
+
+        // we don't know the exact post id - only that it will be > 100
+        // since JSONPlaceholder has built-in 100 posts
+        expect(response.body).property('id').to.be.a('number')
+          .and.to.be.gt(100)
+
+        // we don't know the user id here - since it was in above closure
+        // so in this test just confirm that the property is there
+        expect(response.body).property('userId').to.be.a('number')
+      })
+  })
+
+  it('cy.request() - save response in the shared test context', () => {
+    // https://on.cypress.io/variables-and-aliases
+    cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
+      .its('body').its('0') // yields the first element of the returned list
+      .as('user') // saves the object in the test context
+      .then(function () {
+        // NOTE 👀
+        //  By the time this callback runs the "as('user')" command
+        //  has saved the user object in the test context.
+        //  To access the test context we need to use
+        //  the "function () { ... }" callback form,
+        //  otherwise "this" points at a wrong or undefined object!
+        cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
+          userId: this.user.id,
+          title: 'Cypress Test Runner',
+          body: 'Fast, easy and reliable testing for anything that runs in a browser.',
+        })
+        .its('body').as('post') // save the new post from the response
+      })
+      .then(function () {
+        // When this callback runs, both "cy.request" API commands have finished
+        // and the test context has "user" and "post" objects set.
+        // Let's verify them.
+        expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
+      })
+  })
+
+  it('cy.intercept() - route responses to matching requests', () => {
+    // https://on.cypress.io/intercept
+
+    let message = 'whoa, this comment does not exist'
+
+    // Listen to GET to comments/1
+    cy.intercept('GET', '**/comments/*').as('getComment')
+
+    // we have code that gets a comment when
+    // the button is clicked in scripts.js
+    cy.get('.network-btn').click()
+
+    // https://on.cypress.io/wait
+    cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
+
+    // Listen to POST to comments
+    cy.intercept('POST', '**/comments').as('postComment')
+
+    // we have code that posts a comment when
+    // the button is clicked in scripts.js
+    cy.get('.network-post').click()
+    cy.wait('@postComment').should(({ request, response }) => {
+      expect(request.body).to.include('email')
+      expect(request.headers).to.have.property('content-type')
+      expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
+    })
+
+    // Stub a response to PUT comments/ ****
+    cy.intercept({
+      method: 'PUT',
+      url: '**/comments/*',
+    }, {
+      statusCode: 404,
+      body: { error: message },
+      headers: { 'access-control-allow-origin': '*' },
+      delayMs: 500,
+    }).as('putComment')
+
+    // we have code that puts a comment when
+    // the button is clicked in scripts.js
+    cy.get('.network-put').click()
+
+    cy.wait('@putComment')
+
+    // our 404 statusCode logic in scripts.js executed
+    cy.get('.network-put-comment').should('contain', message)
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/querying.spec.js b/frontend/cypress/integration/2-advanced-examples/querying.spec.js
new file mode 100644
index 0000000..0097048
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/querying.spec.js
@@ -0,0 +1,114 @@
+/// <reference types="cypress" />
+
+context('Querying', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/querying')
+  })
+
+  // The most commonly used query is 'cy.get()', you can
+  // think of this like the '$' in jQuery
+
+  it('cy.get() - query DOM elements', () => {
+    // https://on.cypress.io/get
+
+    cy.get('#query-btn').should('contain', 'Button')
+
+    cy.get('.query-btn').should('contain', 'Button')
+
+    cy.get('#querying .well>button:first').should('contain', 'Button')
+    //              ↲
+    // Use CSS selectors just like jQuery
+
+    cy.get('[data-test-id="test-example"]').should('have.class', 'example')
+
+    // 'cy.get()' yields jQuery object, you can get its attribute
+    // by invoking `.attr()` method
+    cy.get('[data-test-id="test-example"]')
+      .invoke('attr', 'data-test-id')
+      .should('equal', 'test-example')
+
+    // or you can get element's CSS property
+    cy.get('[data-test-id="test-example"]')
+      .invoke('css', 'position')
+      .should('equal', 'static')
+
+    // or use assertions directly during 'cy.get()'
+    // https://on.cypress.io/assertions
+    cy.get('[data-test-id="test-example"]')
+      .should('have.attr', 'data-test-id', 'test-example')
+      .and('have.css', 'position', 'static')
+  })
+
+  it('cy.contains() - query DOM elements with matching content', () => {
+    // https://on.cypress.io/contains
+    cy.get('.query-list')
+      .contains('bananas')
+      .should('have.class', 'third')
+
+    // we can pass a regexp to `.contains()`
+    cy.get('.query-list')
+      .contains(/^b\w+/)
+      .should('have.class', 'third')
+
+    cy.get('.query-list')
+      .contains('apples')
+      .should('have.class', 'first')
+
+    // passing a selector to contains will
+    // yield the selector containing the text
+    cy.get('#querying')
+      .contains('ul', 'oranges')
+      .should('have.class', 'query-list')
+
+    cy.get('.query-button')
+      .contains('Save Form')
+      .should('have.class', 'btn')
+  })
+
+  it('.within() - query DOM elements within a specific element', () => {
+    // https://on.cypress.io/within
+    cy.get('.query-form').within(() => {
+      cy.get('input:first').should('have.attr', 'placeholder', 'Email')
+      cy.get('input:last').should('have.attr', 'placeholder', 'Password')
+    })
+  })
+
+  it('cy.root() - query the root DOM element', () => {
+    // https://on.cypress.io/root
+
+    // By default, root is the document
+    cy.root().should('match', 'html')
+
+    cy.get('.query-ul').within(() => {
+      // In this within, the root is now the ul DOM element
+      cy.root().should('have.class', 'query-ul')
+    })
+  })
+
+  it('best practices - selecting elements', () => {
+    // https://on.cypress.io/best-practices#Selecting-Elements
+    cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
+      // Worst - too generic, no context
+      cy.get('button').click()
+
+      // Bad. Coupled to styling. Highly subject to change.
+      cy.get('.btn.btn-large').click()
+
+      // Average. Coupled to the `name` attribute which has HTML semantics.
+      cy.get('[name=submission]').click()
+
+      // Better. But still coupled to styling or JS event listeners.
+      cy.get('#main').click()
+
+      // Slightly better. Uses an ID but also ensures the element
+      // has an ARIA role attribute
+      cy.get('#main[role=button]').click()
+
+      // Much better. But still coupled to text content that may change.
+      cy.contains('Submit').click()
+
+      // Best. Insulated from all changes.
+      cy.get('[data-cy=submit]').click()
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/spies_stubs_clocks.spec.js b/frontend/cypress/integration/2-advanced-examples/spies_stubs_clocks.spec.js
new file mode 100644
index 0000000..18b643e
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/spies_stubs_clocks.spec.js
@@ -0,0 +1,205 @@
+/// <reference types="cypress" />
+// remove no check once Cypress.sinon is typed
+// https://github.com/cypress-io/cypress/issues/6720
+
+context('Spies, Stubs, and Clock', () => {
+  it('cy.spy() - wrap a method in a spy', () => {
+    // https://on.cypress.io/spy
+    cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+
+    const obj = {
+      foo () {},
+    }
+
+    const spy = cy.spy(obj, 'foo').as('anyArgs')
+
+    obj.foo()
+
+    expect(spy).to.be.called
+  })
+
+  it('cy.spy() retries until assertions pass', () => {
+    cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+
+    const obj = {
+      /**
+       * Prints the argument passed
+       * @param x {any}
+      */
+      foo (x) {
+        console.log('obj.foo called with', x)
+      },
+    }
+
+    cy.spy(obj, 'foo').as('foo')
+
+    setTimeout(() => {
+      obj.foo('first')
+    }, 500)
+
+    setTimeout(() => {
+      obj.foo('second')
+    }, 2500)
+
+    cy.get('@foo').should('have.been.calledTwice')
+  })
+
+  it('cy.stub() - create a stub and/or replace a function with stub', () => {
+    // https://on.cypress.io/stub
+    cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+
+    const obj = {
+      /**
+       * prints both arguments to the console
+       * @param a {string}
+       * @param b {string}
+      */
+      foo (a, b) {
+        console.log('a', a, 'b', b)
+      },
+    }
+
+    const stub = cy.stub(obj, 'foo').as('foo')
+
+    obj.foo('foo', 'bar')
+
+    expect(stub).to.be.called
+  })
+
+  it('cy.clock() - control time in the browser', () => {
+    // https://on.cypress.io/clock
+
+    // create the date in UTC so its always the same
+    // no matter what local timezone the browser is running in
+    const now = new Date(Date.UTC(2017, 2, 14)).getTime()
+
+    cy.clock(now)
+    cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+    cy.get('#clock-div').click()
+      .should('have.text', '1489449600')
+  })
+
+  it('cy.tick() - move time in the browser', () => {
+    // https://on.cypress.io/tick
+
+    // create the date in UTC so its always the same
+    // no matter what local timezone the browser is running in
+    const now = new Date(Date.UTC(2017, 2, 14)).getTime()
+
+    cy.clock(now)
+    cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
+    cy.get('#tick-div').click()
+      .should('have.text', '1489449600')
+
+    cy.tick(10000) // 10 seconds passed
+    cy.get('#tick-div').click()
+      .should('have.text', '1489449610')
+  })
+
+  it('cy.stub() matches depending on arguments', () => {
+    // see all possible matchers at
+    // https://sinonjs.org/releases/latest/matchers/
+    const greeter = {
+      /**
+       * Greets a person
+       * @param {string} name
+      */
+      greet (name) {
+        return `Hello, ${name}!`
+      },
+    }
+
+    cy.stub(greeter, 'greet')
+      .callThrough() // if you want non-matched calls to call the real method
+      .withArgs(Cypress.sinon.match.string).returns('Hi')
+      .withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
+
+    expect(greeter.greet('World')).to.equal('Hi')
+    // @ts-ignore
+    expect(() => greeter.greet(42)).to.throw('Invalid name')
+    expect(greeter.greet).to.have.been.calledTwice
+
+    // non-matched calls goes the actual method
+    // @ts-ignore
+    expect(greeter.greet()).to.equal('Hello, undefined!')
+  })
+
+  it('matches call arguments using Sinon matchers', () => {
+    // see all possible matchers at
+    // https://sinonjs.org/releases/latest/matchers/
+    const calculator = {
+      /**
+       * returns the sum of two arguments
+       * @param a {number}
+       * @param b {number}
+      */
+      add (a, b) {
+        return a + b
+      },
+    }
+
+    const spy = cy.spy(calculator, 'add').as('add')
+
+    expect(calculator.add(2, 3)).to.equal(5)
+
+    // if we want to assert the exact values used during the call
+    expect(spy).to.be.calledWith(2, 3)
+
+    // let's confirm "add" method was called with two numbers
+    expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
+
+    // alternatively, provide the value to match
+    expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
+
+    // match any value
+    expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
+
+    // match any value from a list
+    expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
+
+    /**
+     * Returns true if the given number is event
+     * @param {number} x
+     */
+    const isEven = (x) => x % 2 === 0
+
+    // expect the value to pass a custom predicate function
+    // the second argument to "sinon.match(predicate, message)" is
+    // shown if the predicate does not pass and assertion fails
+    expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
+
+    /**
+     * Returns a function that checks if a given number is larger than the limit
+     * @param {number} limit
+     * @returns {(x: number) => boolean}
+     */
+    const isGreaterThan = (limit) => (x) => x > limit
+
+    /**
+     * Returns a function that checks if a given number is less than the limit
+     * @param {number} limit
+     * @returns {(x: number) => boolean}
+     */
+    const isLessThan = (limit) => (x) => x < limit
+
+    // you can combine several matchers using "and", "or"
+    expect(spy).to.be.calledWith(
+      Cypress.sinon.match.number,
+      Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
+    )
+
+    expect(spy).to.be.calledWith(
+      Cypress.sinon.match.number,
+      Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
+    )
+
+    // matchers can be used from BDD assertions
+    cy.get('@add').should('have.been.calledWith',
+      Cypress.sinon.match.number, Cypress.sinon.match(3))
+
+    // you can alias matchers for shorter test code
+    const { match: M } = Cypress.sinon
+
+    cy.get('@add').should('have.been.calledWith', M.number, M(3))
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/traversal.spec.js b/frontend/cypress/integration/2-advanced-examples/traversal.spec.js
new file mode 100644
index 0000000..0a3b9d3
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/traversal.spec.js
@@ -0,0 +1,121 @@
+/// <reference types="cypress" />
+
+context('Traversal', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/traversal')
+  })
+
+  it('.children() - get child DOM elements', () => {
+    // https://on.cypress.io/children
+    cy.get('.traversal-breadcrumb')
+      .children('.active')
+      .should('contain', 'Data')
+  })
+
+  it('.closest() - get closest ancestor DOM element', () => {
+    // https://on.cypress.io/closest
+    cy.get('.traversal-badge')
+      .closest('ul')
+      .should('have.class', 'list-group')
+  })
+
+  it('.eq() - get a DOM element at a specific index', () => {
+    // https://on.cypress.io/eq
+    cy.get('.traversal-list>li')
+      .eq(1).should('contain', 'siamese')
+  })
+
+  it('.filter() - get DOM elements that match the selector', () => {
+    // https://on.cypress.io/filter
+    cy.get('.traversal-nav>li')
+      .filter('.active').should('contain', 'About')
+  })
+
+  it('.find() - get descendant DOM elements of the selector', () => {
+    // https://on.cypress.io/find
+    cy.get('.traversal-pagination')
+      .find('li').find('a')
+      .should('have.length', 7)
+  })
+
+  it('.first() - get first DOM element', () => {
+    // https://on.cypress.io/first
+    cy.get('.traversal-table td')
+      .first().should('contain', '1')
+  })
+
+  it('.last() - get last DOM element', () => {
+    // https://on.cypress.io/last
+    cy.get('.traversal-buttons .btn')
+      .last().should('contain', 'Submit')
+  })
+
+  it('.next() - get next sibling DOM element', () => {
+    // https://on.cypress.io/next
+    cy.get('.traversal-ul')
+      .contains('apples').next().should('contain', 'oranges')
+  })
+
+  it('.nextAll() - get all next sibling DOM elements', () => {
+    // https://on.cypress.io/nextall
+    cy.get('.traversal-next-all')
+      .contains('oranges')
+      .nextAll().should('have.length', 3)
+  })
+
+  it('.nextUntil() - get next sibling DOM elements until next el', () => {
+    // https://on.cypress.io/nextuntil
+    cy.get('#veggies')
+      .nextUntil('#nuts').should('have.length', 3)
+  })
+
+  it('.not() - remove DOM elements from set of DOM elements', () => {
+    // https://on.cypress.io/not
+    cy.get('.traversal-disabled .btn')
+      .not('[disabled]').should('not.contain', 'Disabled')
+  })
+
+  it('.parent() - get parent DOM element from DOM elements', () => {
+    // https://on.cypress.io/parent
+    cy.get('.traversal-mark')
+      .parent().should('contain', 'Morbi leo risus')
+  })
+
+  it('.parents() - get parent DOM elements from DOM elements', () => {
+    // https://on.cypress.io/parents
+    cy.get('.traversal-cite')
+      .parents().should('match', 'blockquote')
+  })
+
+  it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
+    // https://on.cypress.io/parentsuntil
+    cy.get('.clothes-nav')
+      .find('.active')
+      .parentsUntil('.clothes-nav')
+      .should('have.length', 2)
+  })
+
+  it('.prev() - get previous sibling DOM element', () => {
+    // https://on.cypress.io/prev
+    cy.get('.birds').find('.active')
+      .prev().should('contain', 'Lorikeets')
+  })
+
+  it('.prevAll() - get all previous sibling DOM elements', () => {
+    // https://on.cypress.io/prevall
+    cy.get('.fruits-list').find('.third')
+      .prevAll().should('have.length', 2)
+  })
+
+  it('.prevUntil() - get all previous sibling DOM elements until el', () => {
+    // https://on.cypress.io/prevuntil
+    cy.get('.foods-list').find('#nuts')
+      .prevUntil('#veggies').should('have.length', 3)
+  })
+
+  it('.siblings() - get all sibling DOM elements', () => {
+    // https://on.cypress.io/siblings
+    cy.get('.traversal-pills .active')
+      .siblings().should('have.length', 2)
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/utilities.spec.js b/frontend/cypress/integration/2-advanced-examples/utilities.spec.js
new file mode 100644
index 0000000..24e61a6
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/utilities.spec.js
@@ -0,0 +1,110 @@
+/// <reference types="cypress" />
+
+context('Utilities', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/utilities')
+  })
+
+  it('Cypress._ - call a lodash method', () => {
+    // https://on.cypress.io/_
+    cy.request('https://jsonplaceholder.cypress.io/users')
+      .then((response) => {
+        let ids = Cypress._.chain(response.body).map('id').take(3).value()
+
+        expect(ids).to.deep.eq([1, 2, 3])
+      })
+  })
+
+  it('Cypress.$ - call a jQuery method', () => {
+    // https://on.cypress.io/$
+    let $li = Cypress.$('.utility-jquery li:first')
+
+    cy.wrap($li)
+      .should('not.have.class', 'active')
+      .click()
+      .should('have.class', 'active')
+  })
+
+  it('Cypress.Blob - blob utilities and base64 string conversion', () => {
+    // https://on.cypress.io/blob
+    cy.get('.utility-blob').then(($div) => {
+      // https://github.com/nolanlawson/blob-util#imgSrcToDataURL
+      // get the dataUrl string for the javascript-logo
+      return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
+      .then((dataUrl) => {
+        // create an <img> element and set its src to the dataUrl
+        let img = Cypress.$('<img />', { src: dataUrl })
+
+        // need to explicitly return cy here since we are initially returning
+        // the Cypress.Blob.imgSrcToDataURL promise to our test
+        // append the image
+        $div.append(img)
+
+        cy.get('.utility-blob img').click()
+          .should('have.attr', 'src', dataUrl)
+      })
+    })
+  })
+
+  it('Cypress.minimatch - test out glob patterns against strings', () => {
+    // https://on.cypress.io/minimatch
+    let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
+      matchBase: true,
+    })
+
+    expect(matching, 'matching wildcard').to.be.true
+
+    matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
+      matchBase: true,
+    })
+
+    expect(matching, 'comments').to.be.false
+
+    // ** matches against all downstream path segments
+    matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
+      matchBase: true,
+    })
+
+    expect(matching, 'comments').to.be.true
+
+    // whereas * matches only the next path segment
+
+    matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
+      matchBase: false,
+    })
+
+    expect(matching, 'comments').to.be.false
+  })
+
+  it('Cypress.Promise - instantiate a bluebird promise', () => {
+    // https://on.cypress.io/promise
+    let waited = false
+
+    /**
+     * @return Bluebird<string>
+     */
+    function waitOneSecond () {
+      // return a promise that resolves after 1 second
+      // @ts-ignore TS2351 (new Cypress.Promise)
+      return new Cypress.Promise((resolve, reject) => {
+        setTimeout(() => {
+          // set waited to true
+          waited = true
+
+          // resolve with 'foo' string
+          resolve('foo')
+        }, 1000)
+      })
+    }
+
+    cy.then(() => {
+      // return a promise to cy.then() that
+      // is awaited until it resolves
+      // @ts-ignore TS7006
+      return waitOneSecond().then((str) => {
+        expect(str).to.eq('foo')
+        expect(waited).to.be.true
+      })
+    })
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/viewport.spec.js b/frontend/cypress/integration/2-advanced-examples/viewport.spec.js
new file mode 100644
index 0000000..dbcd7ee
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/viewport.spec.js
@@ -0,0 +1,59 @@
+/// <reference types="cypress" />
+
+context('Viewport', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/viewport')
+  })
+
+  it('cy.viewport() - set the viewport size and dimension', () => {
+    // https://on.cypress.io/viewport
+
+    cy.get('#navbar').should('be.visible')
+    cy.viewport(320, 480)
+
+    // the navbar should have collapse since our screen is smaller
+    cy.get('#navbar').should('not.be.visible')
+    cy.get('.navbar-toggle').should('be.visible').click()
+    cy.get('.nav').find('a').should('be.visible')
+
+    // lets see what our app looks like on a super large screen
+    cy.viewport(2999, 2999)
+
+    // cy.viewport() accepts a set of preset sizes
+    // to easily set the screen to a device's width and height
+
+    // We added a cy.wait() between each viewport change so you can see
+    // the change otherwise it is a little too fast to see :)
+
+    cy.viewport('macbook-15')
+    cy.wait(200)
+    cy.viewport('macbook-13')
+    cy.wait(200)
+    cy.viewport('macbook-11')
+    cy.wait(200)
+    cy.viewport('ipad-2')
+    cy.wait(200)
+    cy.viewport('ipad-mini')
+    cy.wait(200)
+    cy.viewport('iphone-6+')
+    cy.wait(200)
+    cy.viewport('iphone-6')
+    cy.wait(200)
+    cy.viewport('iphone-5')
+    cy.wait(200)
+    cy.viewport('iphone-4')
+    cy.wait(200)
+    cy.viewport('iphone-3')
+    cy.wait(200)
+
+    // cy.viewport() accepts an orientation for all presets
+    // the default orientation is 'portrait'
+    cy.viewport('ipad-2', 'portrait')
+    cy.wait(200)
+    cy.viewport('iphone-4', 'landscape')
+    cy.wait(200)
+
+    // The viewport will be reset back to the default dimensions
+    // in between tests (the  default can be set in cypress.json)
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/waiting.spec.js b/frontend/cypress/integration/2-advanced-examples/waiting.spec.js
new file mode 100644
index 0000000..c8f0d7c
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/waiting.spec.js
@@ -0,0 +1,31 @@
+/// <reference types="cypress" />
+
+context('Waiting', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/waiting')
+  })
+  // BE CAREFUL of adding unnecessary wait times.
+  // https://on.cypress.io/best-practices#Unnecessary-Waiting
+
+  // https://on.cypress.io/wait
+  it('cy.wait() - wait for a specific amount of time', () => {
+    cy.get('.wait-input1').type('Wait 1000ms after typing')
+    cy.wait(1000)
+    cy.get('.wait-input2').type('Wait 1000ms after typing')
+    cy.wait(1000)
+    cy.get('.wait-input3').type('Wait 1000ms after typing')
+    cy.wait(1000)
+  })
+
+  it('cy.wait() - wait for a specific route', () => {
+    // Listen to GET to comments/1
+    cy.intercept('GET', '**/comments/*').as('getComment')
+
+    // we have code that gets a comment when
+    // the button is clicked in scripts.js
+    cy.get('.network-btn').click()
+
+    // wait for GET comments/1
+    cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
+  })
+})
diff --git a/frontend/cypress/integration/2-advanced-examples/window.spec.js b/frontend/cypress/integration/2-advanced-examples/window.spec.js
new file mode 100644
index 0000000..f94b649
--- /dev/null
+++ b/frontend/cypress/integration/2-advanced-examples/window.spec.js
@@ -0,0 +1,22 @@
+/// <reference types="cypress" />
+
+context('Window', () => {
+  beforeEach(() => {
+    cy.visit('https://example.cypress.io/commands/window')
+  })
+
+  it('cy.window() - get the global window object', () => {
+    // https://on.cypress.io/window
+    cy.window().should('have.property', 'top')
+  })
+
+  it('cy.document() - get the document object', () => {
+    // https://on.cypress.io/document
+    cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
+  })
+
+  it('cy.title() - get the title', () => {
+    // https://on.cypress.io/title
+    cy.title().should('include', 'Kitchen Sink')
+  })
+})
diff --git a/frontend/cypress/integration/FR5_spec.js b/frontend/cypress/integration/FR5_spec.js
new file mode 100644
index 0000000..4105ae1
--- /dev/null
+++ b/frontend/cypress/integration/FR5_spec.js
@@ -0,0 +1,103 @@
+/**
+ * The user should be able to view all of the details, files, and comments 
+ * on workouts of sufficient visibility. 
+ * For athletes, this means that the workout needs to be either their own or public. 
+ * For coaches, this means that the workout is at least one 
+ * of their athletes' non-private workouts OR the workout is public. 
+ * For visitors, this means that the workout needs to be public.
+ */
+
+Cypress.Commands.add('login', (username, password) => {
+    cy.visit('/login.html')
+    cy.get('input[name="username"]').type(username)
+    cy.get('input[name="password"]').type(password)
+    cy.get('#btn-login').click()
+    cy.url().should('contain', '/workouts.html')
+    cy.intercept('/api/workouts/*').as('getWorkouts')
+    cy.wait('@getWorkouts')
+  })
+
+Cypress.Commands.add('logout', () => {
+  cy.visit('/logout.html')
+  cy.url().should('contain', '/index.html')
+})
+
+describe('Testing group functionality and fitness profile', () => {
+  beforeEach(() => {
+    // Two use profiles with below info are needed to run tests.
+    // athlete needs to have user "coach" as coach.
+    cy.login('athlete', 'secure')
+    // cy.login('coach', 'secure')
+
+    cy.get('#btn-create-workout').click()
+    cy.get('input[name="name"]').type("My public workout")
+    cy.get('input[name="date"]').type('2017-06-01T08:30')
+    cy.get('textarea[name="notes"]').type('This is a note')
+    cy.get('#inputVisibility').select('Public')
+    cy.get('#btn-ok-workout').click()
+    cy.intercept('/api/workouts/').as('getWorkouts')
+    cy.wait('@getWorkouts')
+
+    cy.get('#btn-create-workout').click()
+    cy.get('input[name="name"]').type("My private workout")
+    cy.get('input[name="date"]').type('2017-06-01T08:30')
+    cy.get('textarea[name="notes"]').type('This is a note')
+    cy.get('#inputVisibility').select('Private')
+    cy.get('#btn-ok-workout').click()
+    cy.intercept('/api/workouts/').as('getWorkouts')
+    cy.wait('@getWorkouts')
+
+    cy.get('#btn-create-workout').click()
+    cy.get('input[name="name"]').type("My coach workout")
+    cy.get('input[name="date"]').type('2017-06-01T08:30')
+    cy.get('textarea[name="notes"]').type('This is a note')
+    cy.get('#inputVisibility').select('Coach')
+    cy.get('#btn-ok-workout').click()
+    cy.intercept('/api/workouts/').as('getWorkouts')
+    cy.wait('@getWorkouts')
+
+
+  })
+
+  afterEach(() => {
+    /*
+    cy.visit('/workouts.html')
+    cy.contains('My public workout').click()
+    cy.get('#btn-edit-workout').click()
+    cy.get('#btn-delete-workout').click()
+    cy.contains('My private workout').click()
+    cy.get('#btn-edit-workout').click()
+    cy.get('#btn-delete-workout').click()
+    cy.contains('My coach workout').click()
+    cy.get('#btn-edit-workout').click()
+    cy.get('#btn-delete-workout').click()
+    */
+
+    cy.logout()
+  })
+
+  it('Testing visibility', () => {
+    // testing visibility as owner
+    cy.contains('My public workout')
+    //cy.contains('My coach workout')
+    //cy.contains('My private workout')
+
+    // testing visibility as coach
+    cy.logout()
+    cy.login('coach', 'secure')
+    cy.contains('My public workout')
+    cy.contains('My coach workout')
+
+    // testing visibility as another athelete
+    cy.logout()
+    cy.login('athlete2', 'secure')
+    cy.contains('My public workout')
+
+    // testing visibility as visitor
+    cy.logout()
+    cy.visit('workouts.html')
+    cy.intercept('/api/workouts/*').as('getWorkouts')
+    cy.wait('@getWorkouts')
+    //cy.contains('My public workout')
+  })
+})
\ No newline at end of file
diff --git a/frontend/cypress/integration/edit_exercise_spec.js b/frontend/cypress/integration/edit_exercise_spec.js
new file mode 100644
index 0000000..0a7e0dd
--- /dev/null
+++ b/frontend/cypress/integration/edit_exercise_spec.js
@@ -0,0 +1,76 @@
+Cypress.Commands.add('login', (username, password) => {
+  cy.visit('/login.html')
+  cy.get('input[name="username"]').type(username)
+  cy.get('input[name="password"]').type(password)
+  cy.get('#btn-login').click()
+  cy.url().should('contain', '/workouts.html')
+})
+
+describe('Testing boundary value on view/edit exercise page', () => {
+  beforeEach(() => {
+    //wait for browser to store tokens.
+    cy.intercept('/api/workouts/*').as('getWorkouts')
+    cy.login('testuser', 'secure')
+    cy.wait('@getWorkouts')
+    // wait for the exercise to be rertrieved from backend.
+    cy.visit('/exercise.html?id=1')
+    cy.intercept('/api/exercises/*').as('getExercises')
+    cy.wait('@getExercises')
+    // enable editing
+    cy.get('#btn-edit-exercise').click()
+  })
+
+ 
+
+  /**
+   * The backend model states that there is an upper boundary of 50 characters.
+   * And that it cannot be empty. meaning boundary of 1-50 character length.
+   * This is not enforces in the HTML form, but only backend. 
+   */
+  it('Testing upper boundaries for input length for Unit', () => {
+
+    // testing for 51
+    cy.get('input[name="unit"]').clear().type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY651')
+    cy.get('#btn-ok-exercise').click()
+    cy.contains('Ensure this field has no more than 50 characters.')
+    // testing for 50
+    cy.get('input[name="unit"]').clear().type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY65')
+    cy.get('#btn-ok-exercise').click()
+    // if valid input then edit button should appear again after clickin the OK button
+    cy.wait('@getExercises')
+    cy.get('#btn-edit-exercise').should('be.visible')
+  })
+
+  it('Testing lower boundaries for input length for Unit', () => {
+    // testing for 0
+    cy.get('input[name="unit"]').clear()
+    cy.get('#btn-ok-exercise').click()
+    cy.contains('This field may not be blank.')
+    // testing for 1
+    cy.get('input[name="unit"]').clear().type('1')
+    cy.get('#btn-ok-exercise').click()
+    // if valid input then edit button should appear again after clickin the OK button
+    cy.get('#btn-edit-exercise').should('be.visible')
+  })
+
+   /**
+   * The lower boundaries on duration and calories are not enforced and
+   * therefore the tests will fail.
+   * The tests are testing that the user should be shown a front end validation message.
+   */
+
+  it('Testing lower boundaries for duration and calories', () => {
+    cy.get('input[name="duration"]').clear().type('-1').then(($input) => {
+      expect($input[0].validationMessage).to.equal('[Error message]')
+    })
+    cy.get('input[name="duration"]').clear().type('0').then(($input) => {
+      expect($input[0].validationMessage).to.equal('')
+    })
+    cy.get('input[name="calories"]').clear().type('-1').then(($input) => {
+      expect($input[0].validationMessage).to.equal('[Error message]')
+    })
+    cy.get('input[name="calories"]').clear().type('0').then(($input) => {
+      expect($input[0].validationMessage).to.equal('')
+    })
+  })
+})
\ No newline at end of file
diff --git a/frontend/cypress/integration/integration_test_spec.js b/frontend/cypress/integration/integration_test_spec.js
new file mode 100644
index 0000000..eb6b51c
--- /dev/null
+++ b/frontend/cypress/integration/integration_test_spec.js
@@ -0,0 +1,132 @@
+Cypress.Commands.add('login', (username, password) => {
+  cy.visit('/login.html')
+  cy.get('input[name="username"]').type(username)
+  cy.get('input[name="password"]').type(password)
+  cy.get('#btn-login').click()
+  cy.url().should('contain', '/workouts.html')
+  cy.intercept('/api/workouts/*').as('getWorkouts')
+  cy.wait('@getWorkouts')
+})
+
+describe('Testing group functionality and fitness profile', () => {
+  beforeEach(() => {
+    cy.login('testuser', 'secure')
+    // wait for the exercise to be rertrieved from backend.
+    cy.visit('/groups.html')
+    cy.intercept('/api/groups/').as('getGroups')
+    cy.wait('@getGroups')
+  })
+
+  it('Testing creating new group', () => {
+    cy.get('#btn-create-group').click()
+    cy.url().should('contain', '/group.html')
+    
+    // typing data for new group
+    cy.get('input[name="name"]').type('My Group')
+    cy.get('textarea[name="description"]').type('This is my group')
+    cy.get('#btn-ok-group').click()
+
+    // waiting for response from backend and checking if group was created.
+    cy.url().should('contain', '/groups.html')
+    cy.intercept('/api/groups/').as('getGroups')
+    cy.wait('@getGroups')
+    cy.contains('My Group')
+    cy.contains('This is my group')
+  })
+
+  it('Testing editing group', () => {
+    // Finding the group to edit and clicking on it
+    cy.url().should('contain', '/groups.html')
+    cy.contains('My Group').click()
+    cy.url().should('include', '/groupContent.html?id=')
+    
+    // clicking on the edit button and waiting until the data has loaded
+    cy.get('#btn-edit-group').click()
+    cy.url().should('include', '/group.html?id=')
+    cy.intercept('/api/groups/*').as('getGroup')
+    cy.wait('@getGroup')
+
+    // updating the group info
+    cy.get('#btn-edit-group').click()
+    cy.get('input[name="name"]').clear().type('My Group (edited)')
+    cy.get('textarea[name="description"]').clear().type('This is my group (edited)')
+
+    // confirming the edit and checking that i is updated
+    cy.get('#btn-ok-group').click()
+    cy.visit('/groups.html')
+    cy.wait('@getGroups')
+    cy.contains('My Group (edited)')
+    cy.contains('This is my group (edited)')
+  })
+
+  it('Testing adding content to group', () => {
+    cy.contains('My Group (edited)').click()
+    cy.url().should('include', '/groupContent.html?id=')
+    cy.get('#btn-add-content').click()
+    cy.get('input[name="title"]').type('My Content')
+    cy.get('textarea[name="description"]').type('This is content description')
+    
+    // typing data for new group
+    cy.get('#btn-ok-addcontent').click()
+    cy.url().should('contain', '/groupContent.html?id=')
+    cy.intercept('/api/content/*').as('getContent')
+    cy.wait('@getContent')
+    cy.contains('My Content')
+    cy.contains('This is content description')
+  })
+
+  it('Testing adding comments and liking group content', () => {
+    // finding the right group and content to post a comment to.
+    cy.contains('My Group (edited)').click()
+    cy.intercept('/api/content/*').as('getContent')
+    cy.wait('@getContent')
+    cy.contains('My Content').click()
+    cy.url().should('include', '/contentcomments.html?id=')
+
+    // typing and adding a comment
+    cy.get('textarea[name="comment"]').type('This is a comment')
+    cy.get('#btn-add-comment').click()
+    cy.intercept('/api/comment/*').as('getComments')
+    cy.wait('@getComments').then((interception) => {
+      assert.isNotNull(interception.response.body, 'API call has data')
+    })
+    // check if comment was added
+    cy.contains('This is a comment')
+
+    /*
+    // liking content
+    cy.get('#btn-like').click()
+    cy.intercept('/api/like/').as('like')
+    cy.wait('@like').its('response.statusCode').should('eq', 200)
+    */
+  })
+
+  it('Testing adding info for fitness profile', () => {
+    // wait for fitness profile data to load from backend
+    cy.visit('/profile.html')
+    cy.intercept('/api/users/*').as('getFitnessProfile')
+    cy.wait('@getFitnessProfile').then((interception) => {
+      assert.isNotNull(interception.response.body, 'API call has data')
+    })
+    cy.url().should('contain', '/profile.html')
+    cy.wait('@getFitnessProfile')
+    cy.get('#btn-edit-profile').click()
+    
+    // typing data for updating profile
+    cy.get('input[name="age"]').clear().type('26')
+    cy.get('input[name="expirience"]').clear().type('5')
+    cy.get('textarea[name="favorite_dicipline"]').clear().type('Crossfit')
+    cy.get('textarea[name="bio"]').clear().type('This is my bio')
+    cy.get('#btn-ok-profile').click()
+
+    cy.wait('@getFitnessProfile').then((interception) => {
+      assert.isNotNull(interception.response.body, 'API call has data')
+    })
+    // checking that the data was updated.
+    cy.get('input[name="age"]').should('have.value', '26')
+    cy.get('input[name="expirience"]').should('have.value', '5')
+    cy.get('textarea[name="favorite_dicipline"]').should('have.value', 'Crossfit')
+    cy.get('textarea[name="bio"]').should('have.value', 'This is my bio')
+  })
+
+})
\ No newline at end of file
diff --git a/frontend/cypress/integration/register_spec.js b/frontend/cypress/integration/register_spec.js
new file mode 100644
index 0000000..3dc6558
--- /dev/null
+++ b/frontend/cypress/integration/register_spec.js
@@ -0,0 +1,121 @@
+describe('Testing boundary value on register page', () => {
+  beforeEach(() => {
+    cy.visit('/register.html')
+
+    /*
+    cy.get('input[name="username"]')
+      .type('simen')
+    cy.get('input[name="email"]')
+      .type('test@test.test')
+    cy.get('input[name="password"]')
+      .type('secure')
+    cy.get('input[name="password1"]')
+      .type('secure')
+    cy.get('input[name="phone_number"]')
+      .type('12345678')
+    cy.get('input[name="country"]')
+      .type('Norway')
+    cy.get('input[name="city"]')
+      .type('Trondheim')
+    cy.get('input[name="street_address"]')
+      .type('My Street')
+    */
+  
+  })
+
+  it('Testing that all HTML form fields have lower boundary length 1', () => {
+    // Sending an empty form should give 8 invalid input validation errors
+    cy.get('#btn-create-account')
+      .click()
+    // Since all are required, then 8 fields should be invalid.
+    cy.get('input:invalid').should('have.length', 8)
+  })
+
+  it('Testing input length lower boundaries for username and passwords', () => {
+    // Username and password length lower bound is 1 because backend model field is required
+    cy.get('input[name="username"]').clear()
+    cy.get('input[name="password"]').clear()
+    cy.get('input[name="password1"]').clear()
+    cy.get('#btn-create-account').click()
+    // checks if error has been raised and rendered to user
+    cy.contains('Registration failed!')
+    cy.contains('username')
+    cy.contains('password')
+    cy.contains('password1')
+    cy.contains('This field may not be blank.')
+
+    cy.visit('/register.html')
+    // try with input length 1, the errors should not be there because lower bound i 1
+    cy.get('input[name="username"]').type('t')
+    cy.get('input[name="password"]').type('t')
+    cy.get('input[name="password1"]').type('t')
+    cy.get('#btn-create-account').click()
+    cy.contains('This field may not be blank.').should('not.exist')
+  })
+
+  it('Testing input length upper boundaries for phone_number, country, city and street', () => {
+    // these fields have no lower boundary in backend.
+    // testing with 51 characters
+    cy.get('input[name="phone_number"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY651')
+    cy.get('input[name="country"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY651')
+    cy.get('input[name="city"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY651')
+    cy.get('input[name="street_address"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY651')
+    cy.get('#btn-create-account').click()
+    // checks if error has been raised and rendered to user
+    cy.contains('Registration failed!')
+    cy.contains('country')
+    cy.contains('city')
+    cy.contains('street_address')
+    cy.contains('Ensure this field has no more than 50 characters.')
+
+    cy.visit('/register.html')
+    // try with input length 50, the errors should not be there because lower bound is 50
+    cy.get('input[name="username"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY65')
+    cy.get('input[name="password"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY65')
+    cy.get('input[name="password1"]').type('Ye9cxDpRYmw1kaQoFxTtMs40jUgo0SiS63PyqFiZzjzerqcY65')
+    cy.get('#btn-create-account').click()
+    cy.contains('Ensure this field has no more than 50 characters.').should('not.exist')
+  })
+
+  it('Testing that email field is the right format', () => {
+    // Invalid email input should raise validation error message
+    cy.get('input[name="email"]').clear().type('not_an_email').then(($input) => {
+      expect($input[0].validationMessage).to.not.equal('')
+    })
+
+    // Sending wrong input is not approved and should not redirect
+    cy.get('#btn-create-account').click()
+    cy.contains('Enter a valid email address.')
+    cy.url().should('be.equal', 'http://localhost:9090/register.html')
+
+    // checking valid email input
+    cy.get('input[name="email"]').clear().type('an_email@test.test').then(($input) => {
+      expect($input[0].validationMessage).to.equal('')
+    })
+  })
+
+/**
+ * There are very few boundaries in the code so we ave assumed that some exist.
+ * We assume that the phone number can have max 10 digits and minimum 7.
+ * This means that the boundary is:
+ * 00000 00000 - 99999 99999
+ * This test will fail because no such boundary exists in the code. 
+ */
+  it('Testing phone number boundary value', () => {
+    // Invalid phone number with 6 digits should return validation error
+    cy.get('input[name="phone_number"]').clear().type('999999').then(($input) => {
+      expect($input[0].validationMessage).to.not.equal('')
+    })
+    // and should not be approved backend and redirected
+    cy.get('#btn-create-account').click()
+    cy.url().should('be.equal', 'http://localhost:9090/register.html')
+
+    // Invalid phone number with 11 digits should return validation error
+    cy.get('input[name="phone_number"]').clear().type('10 000 000 000').then(($input) => {
+      expect($input[0].validationMessage).to.not.equal('')
+    })
+    // and should not be approved backend and redirected
+    cy.get('#btn-create-account').click()
+    cy.url().should('be.equal', 'http://localhost:9090/register.html')
+  })
+})
\ No newline at end of file
diff --git a/frontend/cypress/integration/sample_spec.js b/frontend/cypress/integration/sample_spec.js
new file mode 100644
index 0000000..4157d58
--- /dev/null
+++ b/frontend/cypress/integration/sample_spec.js
@@ -0,0 +1,20 @@
+// Arrange - setup initial app state
+// - Visit a web page
+// - query for an element
+// Act - taken an action
+// -interanct with that element
+// Assert - make an assertion
+// - make assertion about page content
+
+
+describe('My First Test', () => {
+  it('Finds an element', () => {
+    cy.visit('/register.html')
+
+    cy.get('input[name="username"]')
+      .type('simen')
+      .should('have.value', 'simen')
+  })
+})
+
+  
\ No newline at end of file
diff --git a/frontend/cypress/plugins/index.js b/frontend/cypress/plugins/index.js
new file mode 100644
index 0000000..59b2bab
--- /dev/null
+++ b/frontend/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+/// <reference types="cypress" />
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+  // `on` is used to hook into various events Cypress emits
+  // `config` is the resolved Cypress config
+}
diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js
new file mode 100644
index 0000000..119ab03
--- /dev/null
+++ b/frontend/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/frontend/cypress/support/index.js b/frontend/cypress/support/index.js
new file mode 100644
index 0000000..d07e20e
--- /dev/null
+++ b/frontend/cypress/support/index.js
@@ -0,0 +1,25 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+Cypress.on('uncaught:exception', (err, runnable) => {
+    // returning false here prevents Cypress from failing the test
+    return false
+  })
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 684c2b9..0f2f11c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -4,6 +4,67 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@cypress/request": {
+      "version": "2.88.10",
+      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
+      "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
+      "dev": true,
+      "requires": {
+        "aws-sign2": "~0.7.0",
+        "aws4": "^1.8.0",
+        "caseless": "~0.12.0",
+        "combined-stream": "~1.0.6",
+        "extend": "~3.0.2",
+        "forever-agent": "~0.6.1",
+        "form-data": "~2.3.2",
+        "http-signature": "~1.3.6",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "performance-now": "^2.1.0",
+        "qs": "~6.5.2",
+        "safe-buffer": "^5.1.2",
+        "tough-cookie": "~2.5.0",
+        "tunnel-agent": "^0.6.0",
+        "uuid": "^8.3.2"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.3",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
+          "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
+          "dev": true
+        }
+      }
+    },
+    "@cypress/xvfb": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+      "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+      "dev": true,
+      "requires": {
+        "debug": "^3.1.0",
+        "lodash.once": "^4.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.2.7",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+          "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+          "dev": true
+        }
+      }
+    },
     "@netflix/nerror": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/@netflix/nerror/-/nerror-1.1.3.tgz",
@@ -41,6 +102,34 @@
         "fastq": "^1.6.0"
       }
     },
+    "@types/node": {
+      "version": "14.18.12",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.12.tgz",
+      "integrity": "sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==",
+      "dev": true
+    },
+    "@types/sinonjs__fake-timers": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
+      "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==",
+      "dev": true
+    },
+    "@types/sizzle": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+      "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+      "dev": true
+    },
+    "@types/yauzl": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
+      "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -57,6 +146,16 @@
         "negotiator": "0.6.2"
       }
     },
+    "aggregate-error": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+      "dev": true,
+      "requires": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      }
+    },
     "android-versions": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/android-versions/-/android-versions-1.5.0.tgz",
@@ -72,6 +171,27 @@
       "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=",
       "dev": true
     },
+    "ansi-colors": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+      "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+      "dev": true
+    },
+    "ansi-escapes": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+      "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.21.3"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true
+    },
     "ansi-styles": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -81,24 +201,69 @@
         "color-convert": "^1.9.0"
       }
     },
+    "arch": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+      "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+      "dev": true
+    },
     "array-flatten": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
       "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
       "dev": true
     },
+    "asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
     "assert-plus": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
       "dev": true
     },
+    "astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "dev": true
+    },
+    "async": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
+      "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+      "dev": true
+    },
     "at-least-node": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
       "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
       "dev": true
     },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
+      "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
+      "dev": true
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -111,12 +276,33 @@
       "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
       "dev": true
     },
+    "bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+      "dev": true,
+      "requires": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
     "big-integer": {
       "version": "1.6.48",
       "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
       "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==",
       "dev": true
     },
+    "blob-util": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
+      "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==",
+      "dev": true
+    },
+    "bluebird": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+      "dev": true
+    },
     "body-parser": {
       "version": "1.19.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -171,12 +357,40 @@
         "fill-range": "^7.0.1"
       }
     },
+    "buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "dev": true,
+      "requires": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+      "dev": true
+    },
     "bytes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
       "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
       "dev": true
     },
+    "cachedir": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+      "dev": true
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
     "chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -188,6 +402,53 @@
         "supports-color": "^5.3.0"
       }
     },
+    "check-more-types": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+      "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=",
+      "dev": true
+    },
+    "ci-info": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
+      "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==",
+      "dev": true
+    },
+    "clean-stack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+      "dev": true
+    },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^3.1.0"
+      }
+    },
+    "cli-table3": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz",
+      "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==",
+      "dev": true,
+      "requires": {
+        "colors": "1.4.0",
+        "string-width": "^4.2.0"
+      }
+    },
+    "cli-truncate": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+      "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+      "dev": true,
+      "requires": {
+        "slice-ansi": "^3.0.0",
+        "string-width": "^4.2.0"
+      }
+    },
     "color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -203,6 +464,40 @@
       "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
       "dev": true
     },
+    "colorette": {
+      "version": "2.0.16",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
+      "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
+      "dev": true
+    },
+    "colors": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+      "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
+      "dev": true,
+      "optional": true
+    },
+    "combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dev": true,
+      "requires": {
+        "delayed-stream": "~1.0.0"
+      }
+    },
+    "commander": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+      "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+      "dev": true
+    },
+    "common-tags": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+      "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+      "dev": true
+    },
     "compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -447,6 +742,12 @@
         "which": "^1.3.0"
       }
     },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
     "cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -460,6 +761,249 @@
         "which": "^1.2.9"
       }
     },
+    "cypress": {
+      "version": "9.5.2",
+      "resolved": "https://registry.npmjs.org/cypress/-/cypress-9.5.2.tgz",
+      "integrity": "sha512-gYiQYvJozMzDOriUV1rCt6CeRM/pRK4nhwGJj3nJQyX2BoUdTCVwp30xDMKc771HiNVhBtgj5o5/iBdVDVXQUg==",
+      "dev": true,
+      "requires": {
+        "@cypress/request": "^2.88.10",
+        "@cypress/xvfb": "^1.2.4",
+        "@types/node": "^14.14.31",
+        "@types/sinonjs__fake-timers": "8.1.1",
+        "@types/sizzle": "^2.3.2",
+        "arch": "^2.2.0",
+        "blob-util": "^2.0.2",
+        "bluebird": "^3.7.2",
+        "buffer": "^5.6.0",
+        "cachedir": "^2.3.0",
+        "chalk": "^4.1.0",
+        "check-more-types": "^2.24.0",
+        "cli-cursor": "^3.1.0",
+        "cli-table3": "~0.6.1",
+        "commander": "^5.1.0",
+        "common-tags": "^1.8.0",
+        "dayjs": "^1.10.4",
+        "debug": "^4.3.2",
+        "enquirer": "^2.3.6",
+        "eventemitter2": "^6.4.3",
+        "execa": "4.1.0",
+        "executable": "^4.1.1",
+        "extract-zip": "2.0.1",
+        "figures": "^3.2.0",
+        "fs-extra": "^9.1.0",
+        "getos": "^3.2.1",
+        "is-ci": "^3.0.0",
+        "is-installed-globally": "~0.4.0",
+        "lazy-ass": "^1.6.0",
+        "listr2": "^3.8.3",
+        "lodash": "^4.17.21",
+        "log-symbols": "^4.0.0",
+        "minimist": "^1.2.5",
+        "ospath": "^1.2.2",
+        "pretty-bytes": "^5.6.0",
+        "proxy-from-env": "1.0.0",
+        "request-progress": "^3.0.0",
+        "semver": "^7.3.2",
+        "supports-color": "^8.1.1",
+        "tmp": "~0.2.1",
+        "untildify": "^4.0.0",
+        "yauzl": "^2.10.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          },
+          "dependencies": {
+            "supports-color": {
+              "version": "7.2.0",
+              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+              "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+              "dev": true,
+              "requires": {
+                "has-flag": "^4.0.0"
+              }
+            }
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "cross-spawn": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+          "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+          "dev": true,
+          "requires": {
+            "path-key": "^3.1.0",
+            "shebang-command": "^2.0.0",
+            "which": "^2.0.1"
+          }
+        },
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "execa": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+          "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+          "dev": true,
+          "requires": {
+            "cross-spawn": "^7.0.0",
+            "get-stream": "^5.0.0",
+            "human-signals": "^1.1.1",
+            "is-stream": "^2.0.0",
+            "merge-stream": "^2.0.0",
+            "npm-run-path": "^4.0.0",
+            "onetime": "^5.1.0",
+            "signal-exit": "^3.0.2",
+            "strip-final-newline": "^2.0.0"
+          }
+        },
+        "fs-extra": {
+          "version": "9.1.0",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+          "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+          "dev": true,
+          "requires": {
+            "at-least-node": "^1.0.0",
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^6.0.1",
+            "universalify": "^2.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "jsonfile": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+          "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+          "dev": true,
+          "requires": {
+            "graceful-fs": "^4.1.6",
+            "universalify": "^2.0.0"
+          }
+        },
+        "lodash": {
+          "version": "4.17.21",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+          "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        },
+        "path-key": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+          "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+          "dev": true
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "shebang-command": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+          "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+          "dev": true,
+          "requires": {
+            "shebang-regex": "^3.0.0"
+          }
+        },
+        "shebang-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+          "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "8.1.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+          "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "universalify": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+          "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+          "dev": true
+        },
+        "which": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+          "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+          "dev": true,
+          "requires": {
+            "isexe": "^2.0.0"
+          }
+        }
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
+    "dayjs": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz",
+      "integrity": "sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug==",
+      "dev": true
+    },
     "debug": {
       "version": "2.6.9",
       "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -475,6 +1019,12 @@
       "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
       "dev": true
     },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+      "dev": true
+    },
     "depd": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -487,6 +1037,16 @@
       "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
       "dev": true
     },
+    "ecc-jsbn": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+      "dev": true,
+      "requires": {
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -502,6 +1062,12 @@
         "sax": "1.1.4"
       }
     },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
     "encodeurl": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -528,6 +1094,15 @@
         "objectorarray": "^1.0.4"
       }
     },
+    "enquirer": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+      "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+      "dev": true,
+      "requires": {
+        "ansi-colors": "^4.1.1"
+      }
+    },
     "escape-html": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -546,6 +1121,12 @@
       "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
       "dev": true
     },
+    "eventemitter2": {
+      "version": "6.4.5",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
+      "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==",
+      "dev": true
+    },
     "execa": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.3.tgz",
@@ -606,6 +1187,23 @@
         }
       }
     },
+    "executable": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
+      "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==",
+      "dev": true,
+      "requires": {
+        "pify": "^2.2.0"
+      },
+      "dependencies": {
+        "pify": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+          "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+          "dev": true
+        }
+      }
+    },
     "express": {
       "version": "4.17.1",
       "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -644,6 +1242,41 @@
         "vary": "~1.1.2"
       }
     },
+    "extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "dev": true
+    },
+    "extract-zip": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+      "dev": true,
+      "requires": {
+        "@types/yauzl": "^2.9.1",
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
+        "yauzl": "^2.10.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.3.4",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+          "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "extsprintf": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz",
@@ -679,6 +1312,24 @@
         "reusify": "^1.0.4"
       }
     },
+    "fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
+      "dev": true,
+      "requires": {
+        "pend": "~1.2.0"
+      }
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
     "fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -703,6 +1354,23 @@
         "unpipe": "~1.0.0"
       }
     },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+      "dev": true,
+      "requires": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      }
+    },
     "forwarded": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -741,6 +1409,24 @@
         "pump": "^3.0.0"
       }
     },
+    "getos": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+      "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+      "dev": true,
+      "requires": {
+        "async": "^3.2.0"
+      }
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0"
+      }
+    },
     "glob": {
       "version": "7.1.6",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
@@ -764,6 +1450,15 @@
         "is-glob": "^4.0.1"
       }
     },
+    "global-dirs": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
+      "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
+      "dev": true,
+      "requires": {
+        "ini": "2.0.0"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.4",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@@ -797,6 +1492,17 @@
         }
       }
     },
+    "http-signature": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
+      "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^2.0.2",
+        "sshpk": "^1.14.1"
+      }
+    },
     "human-signals": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
@@ -812,6 +1518,18 @@
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
+    "ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true
+    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -828,18 +1546,39 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
       "dev": true
     },
+    "ini": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+      "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+      "dev": true
+    },
     "ipaddr.js": {
       "version": "1.9.1",
       "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
       "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
       "dev": true
     },
+    "is-ci": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+      "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+      "dev": true,
+      "requires": {
+        "ci-info": "^3.2.0"
+      }
+    },
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
       "dev": true
     },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
+    },
     "is-glob": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
@@ -849,18 +1588,46 @@
         "is-extglob": "^2.1.1"
       }
     },
+    "is-installed-globally": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
+      "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
+      "dev": true,
+      "requires": {
+        "global-dirs": "^3.0.0",
+        "is-path-inside": "^3.0.2"
+      }
+    },
     "is-number": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
       "dev": true
     },
+    "is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true
+    },
     "is-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
       "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
       "dev": true
     },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
+      "dev": true
+    },
+    "is-unicode-supported": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+      "dev": true
+    },
     "is-wsl": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
@@ -873,6 +1640,30 @@
       "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
       "dev": true
     },
+    "isstream": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
+      "dev": true
+    },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true
+    },
+    "json-schema": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+      "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+      "dev": true
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
     "jsonfile": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
@@ -882,12 +1673,190 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "jsprim": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+      "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.4.0",
+        "verror": "1.10.0"
+      },
+      "dependencies": {
+        "extsprintf": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+          "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+          "dev": true
+        }
+      }
+    },
+    "lazy-ass": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+      "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=",
+      "dev": true
+    },
+    "listr2": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
+      "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==",
+      "dev": true,
+      "requires": {
+        "cli-truncate": "^2.1.0",
+        "colorette": "^2.0.16",
+        "log-update": "^4.0.0",
+        "p-map": "^4.0.0",
+        "rfdc": "^1.3.0",
+        "rxjs": "^7.5.1",
+        "through": "^2.3.8",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
     "lodash": {
       "version": "4.17.20",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
       "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
       "dev": true
     },
+    "lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=",
+      "dev": true
+    },
+    "log-symbols": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "is-unicode-supported": "^0.1.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
+    "log-update": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+      "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^4.3.0",
+        "cli-cursor": "^3.1.0",
+        "slice-ansi": "^4.0.0",
+        "wrap-ansi": "^6.2.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "slice-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+          "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "astral-regex": "^2.0.0",
+            "is-fullwidth-code-point": "^3.0.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "6.2.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.0.0",
+            "string-width": "^4.1.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
+      }
+    },
+    "lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "requires": {
+        "yallist": "^4.0.0"
+      }
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -964,6 +1933,12 @@
         "brace-expansion": "^1.1.7"
       }
     },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
     "ms": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1079,12 +2054,27 @@
         "os-tmpdir": "^1.0.0"
       }
     },
+    "ospath": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+      "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
+      "dev": true
+    },
     "p-finally": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
       "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
       "dev": true
     },
+    "p-map": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+      "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+      "dev": true,
+      "requires": {
+        "aggregate-error": "^3.0.0"
+      }
+    },
     "p-try": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -1115,6 +2105,18 @@
       "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
       "dev": true
     },
+    "pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
+      "dev": true
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
+      "dev": true
+    },
     "picomatch": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
@@ -1138,6 +2140,12 @@
         "xmldom": "0.1.x"
       }
     },
+    "pretty-bytes": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+      "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+      "dev": true
+    },
     "properties-parser": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.3.1.tgz",
@@ -1157,6 +2165,18 @@
         "ipaddr.js": "1.9.1"
       }
     },
+    "proxy-from-env": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
+      "dev": true
+    },
+    "psl": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+      "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
+      "dev": true
+    },
     "pump": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -1167,6 +2187,12 @@
         "once": "^1.3.1"
       }
     },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+      "dev": true
+    },
     "q": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -1215,18 +2241,61 @@
         "with-open-file": "^0.1.6"
       }
     },
+    "request-progress": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+      "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+      "dev": true,
+      "requires": {
+        "throttleit": "^1.0.0"
+      }
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "dev": true,
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
     "reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true
     },
+    "rfdc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+      "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
+      "dev": true
+    },
+    "rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
     "run-parallel": {
       "version": "1.1.9",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
       "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
       "dev": true
     },
+    "rxjs": {
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
+      "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
+      "dev": true,
+      "requires": {
+        "tslib": "^2.1.0"
+      }
+    },
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -1325,18 +2394,92 @@
       "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
       "dev": true
     },
+    "slice-ansi": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+      "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        }
+      }
+    },
+    "sshpk": {
+      "version": "1.17.0",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
+      "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
+      "dev": true,
+      "requires": {
+        "asn1": "~0.2.3",
+        "assert-plus": "^1.0.0",
+        "bcrypt-pbkdf": "^1.0.0",
+        "dashdash": "^1.12.0",
+        "ecc-jsbn": "~0.1.1",
+        "getpass": "^0.1.1",
+        "jsbn": "~0.1.0",
+        "safer-buffer": "^2.0.2",
+        "tweetnacl": "~0.14.0"
+      }
+    },
     "statuses": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
       "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
       "dev": true
     },
+    "string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      }
+    },
     "string.prototype.codepointat": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
       "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
       "dev": true
     },
+    "strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^5.0.1"
+      }
+    },
     "strip-bom": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -1358,6 +2501,27 @@
         "has-flag": "^3.0.0"
       }
     },
+    "throttleit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+      "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "tmp": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+      "dev": true,
+      "requires": {
+        "rimraf": "^3.0.0"
+      }
+    },
     "to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1373,6 +2537,43 @@
       "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
       "dev": true
     },
+    "tough-cookie": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+      "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+      "dev": true,
+      "requires": {
+        "psl": "^1.1.28",
+        "punycode": "^2.1.1"
+      }
+    },
+    "tslib": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+      "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+      "dev": true
+    },
+    "tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+      "dev": true
+    },
+    "type-fest": {
+      "version": "0.21.3",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+      "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+      "dev": true
+    },
     "type-is": {
       "version": "1.6.18",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1401,18 +2602,41 @@
       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
       "dev": true
     },
+    "untildify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+      "dev": true
+    },
     "utils-merge": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
       "dev": true
     },
+    "uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true
+    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
       "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
       "dev": true
     },
+    "verror": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "^1.2.0"
+      }
+    },
     "which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -1433,6 +2657,43 @@
         "pify": "^4.0.1"
       }
     },
+    "wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        }
+      }
+    },
     "wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -1450,6 +2711,22 @@
       "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
       "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==",
       "dev": true
+    },
+    "yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
+    "yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
     }
   }
 }
diff --git a/frontend/package.json b/frontend/package.json
index 1e59f64..beb2040 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,7 +17,8 @@
     "cordova-browser": "^6.0.0",
     "cordova-plugin-camera": "^4.1.0",
     "cordova-plugin-file": "^6.0.2",
-    "cordova-plugin-whitelist": "^1.3.4"
+    "cordova-plugin-whitelist": "^1.3.4",
+    "cypress": "^9.5.2"
   },
   "cordova": {
     "plugins": {
@@ -31,5 +32,6 @@
       "browser",
       "android"
     ]
-  }
-}
\ No newline at end of file
+  },
+  "dependencies": {}
+}
-- 
GitLab