diff --git a/README.md b/README.md
index c859957a52b964b2ec836da54b4816239f165717..2882a2045fd68fcbd5765d24cf94246d8f258c74 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,31 @@
-# Nightlife App (group23_com2027)
+# NightLyfe (group23_com2027)
 
-Enhance your nightlife experience 
+Enhance your nightlife experience
 
 ## Install the dependencies
+
 ```bash
 npm install
 ```
 
 ### Start the app in development mode (hot-code reloading, error reporting, etc.)
+
 ```bash
 quasar dev
 ```
 
 ### Lint the files
+
 ```bash
 npm run lint
 ```
 
 ### Build the app for production
+
 ```bash
 quasar build
 ```
 
 ### Customize the configuration
+
 See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js).
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1 @@
+{}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
new file mode 100644
index 0000000000000000000000000000000000000000..02e4254378e9785f013be7cc8d94a8229dcbcbb7
--- /dev/null
+++ b/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/cypress/integration/e2e/events.spec.js b/cypress/integration/e2e/events.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a03146992924ade648c237d983b852c4a3e4ca0b
--- /dev/null
+++ b/cypress/integration/e2e/events.spec.js
@@ -0,0 +1,91 @@
+/// <reference types="cypress" />
+
+describe('testing "Events" feature functionality', () => {
+  before(() => {
+    cy.visit('http://localhost:8080')
+  })
+
+  it('should not create event if event name missing', () => {
+    // Click center '+' button
+    cy.get('[data-cy="centerBtn"]').click()
+    // Click new event button
+    cy.get('div').contains('New Event').click()
+    // Check if event form appears
+    cy.get('[data-cy="eventForm"]').should('exist')
+    // Click 'Submit Event Button'
+    cy.get('div').contains('Submit Event').click()
+    // Check for error message
+    cy.get('div').contains('Please insert a name for the event.').should('exist')
+    // Check form has not closed
+    cy.get('[data-cy="eventForm"]').should('exist')
+  })
+
+  it('should not create event if event date missing', () => {
+    // Select name input
+    cy.get('input[aria-label="Event Name"]')
+      .type('Bestest Party')
+      .should('have.value', 'Bestest Party')
+    // Click 'Submit Event Button'
+    cy.get('div').contains('Submit Event').click()
+    // Check for error message
+    cy.get('div').contains('Please insert a valid date for the event.').should('exist')
+    // Check form has not closed
+    cy.get('[data-cy="eventForm"]').should('exist')
+  })
+
+  it('should not create event if event time missing', () => {
+    // Select date input
+    cy.get('input[type="date"]')
+    .type('2021-12-12')
+    .should('have.value', '2021-12-12')
+    // Click 'Submit Event Button'
+    cy.get('div').contains('Submit Event').click()
+    // Check for error message
+    cy.get('div').contains('Please insert a valid time for the event.').should('exist')
+    // Check form has not closed
+    cy.get('[data-cy="eventForm"]').should('exist')
+  })
+
+  it('should not create event if event location missing', () => {
+    // Select time input
+    cy.get('input[type="time"]')
+      .type('12:12')
+      .should('have.value', '12:12')
+    // Click 'Submit Event Button'
+    cy.get('div').contains('Submit Event').click()
+    // Check for error message
+    cy.get('div').contains('Please insert a valid time for the event.').should('exist')
+    // Check form has not closed
+    cy.get('[data-cy="eventForm"]').should('exist')
+  })
+
+  it('should create event', () => {
+    // Select location input
+    cy.get('input[type="search"]')
+    .type('162 Guildford Park', {delay: 100})
+    .should('have.value', '162 Guildford Park')
+    cy.wait(200) // Give time for google Places response
+    // Select location
+    cy.get('span').contains('162 Guildford Park Avenue, Guildford, UK').click()
+    // Add event details
+    cy.get('textarea')
+      .type('Scooby-Doo themed party')
+      .should('have.value', 'Scooby-Doo themed party')
+    // Click submit event button
+    cy.get('div').contains('Submit Event').click()
+    // Check if popup appears after signal 
+    cy.get('div').contains('Event Submitted').should('exist')
+    // Check form has closed
+    cy.get('[data-cy="eventForm"]').should('not.exist')
+  })
+
+  it('should show created event on map', () => {
+    // Give time for firebase response
+    cy.wait(2000) 
+    // Select created event on map
+    cy.get('div[title][role="button"][tabindex=-1]', {force: true}).last().click()
+    // Check event content is correct
+    cy.get('p').contains('Bestest Party (2021-12-12 12:12)').should('exist')
+    cy.get('p').contains('Scooby-Doo themed party').should('exist')
+  })
+})
\ No newline at end of file
diff --git a/cypress/integration/e2e/login.spec.js b/cypress/integration/e2e/login.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..921a11297c78e99d1771e3bb552bcf1c233251d6
--- /dev/null
+++ b/cypress/integration/e2e/login.spec.js
@@ -0,0 +1,23 @@
+/// <reference types="cypress" />
+
+describe('Testing login functionality', () => {
+  beforeEach(() => {
+    indexedDB.deleteDatabase('firebaseLocalStorageDb')
+    cy.visit('http://localhost:8080/#/auth')
+    cy.url().should('include', 'auth')
+  })
+
+  it('Should login', () => {
+    // https://on.cypress.io/type
+    cy.get('[type="email"]')
+      .type('test@test.com')
+      .should('have.value', 'test@test.com')
+
+    cy.get('[type="password"]')
+      .type('123456')
+      .should('have.value', '123456')
+    
+    cy.get('span').contains('Sign In').click()
+    cy.url().should('not.include', 'auth')
+  })
+})
diff --git a/cypress/integration/e2e/signals.spec.js b/cypress/integration/e2e/signals.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..23f178922bdcdf5de0bcc7e8c13855f6a869ad82
--- /dev/null
+++ b/cypress/integration/e2e/signals.spec.js
@@ -0,0 +1,49 @@
+/// <reference types="cypress" />
+
+describe('testing "Signals" feature functionality', () => {
+  before(() => {
+    cy.visit('http://localhost:8080')
+  })
+
+  it('should not create signal if signal type not defined', () => {
+    // Click center '+' button
+    cy.get('[data-cy="centerBtn"]').click()
+    // Click new signal
+    cy.get('div').contains('New Signal').click()
+    // Check if signal form appears
+    cy.get('[data-cy="signalForm"]')
+    // Click 'Create Signal Button'
+    cy.get('div').contains('Create Signal').click()
+    // Check for error message
+    cy.get('div').contains('Please select a signal type.')
+    // Check form has not disappeared 
+    cy.get('[data-cy="signalForm"]').should('exist')
+  })
+
+  it('should create signal', () => {
+    // Select 'Fight Breakout' as signal type
+    cy.get('div').contains('Fight Breakout').click()
+    // Write some details for the signal
+    cy.get('[placeholder="Describe the situation"]')
+      .type('I am writing some details')
+      .should('have.value', 'I am writing some details')
+    // Click 'Create Signal Button'
+    cy.get('div').contains('Create Signal').click()
+    // Check if popup appears after signal 
+    cy.get('div').contains('Signal Sent').should('exist')
+    // Check form disappears on button click
+    cy.get('[data-cy="signalForm"]').should('not.exist')
+  })
+
+  it('should show signal on map', () => {
+    // Give time for firebase response
+    cy.wait(500) 
+    // Click on new signal on the map
+    cy.get('div[title][role="button"][tabindex=-1]', {force: true}).last().click()
+    // Check signal content is correct
+    cy.get('p').contains('Fight Breakout').should('exist')
+    cy.get('p').contains('I am writing some details').should('exist')
+  })
+
+
+})
diff --git a/cypress/integration/examples/actions.spec.js b/cypress/integration/examples/actions.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..092637998e28e31357cb95102f50b2e779db6430
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/aliasing.spec.js b/cypress/integration/examples/aliasing.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a02fb2bb93d7f292d5b72f74a73da64736a7fafe
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/assertions.spec.js b/cypress/integration/examples/assertions.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5ba93d1db6dd62a7851da5f118f90dda58107b93
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/connectors.spec.js b/cypress/integration/examples/connectors.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae8799181d1531938ae92a3d90515ea624dd4383
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/cookies.spec.js b/cypress/integration/examples/cookies.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..31587ff907dea51ccb7c3e6b6293465a9a04de2d
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/cypress_api.spec.js b/cypress/integration/examples/cypress_api.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ec8ceaeda4edef699670761f7fbf30289243c155
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/files.spec.js b/cypress/integration/examples/files.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..01a53235ee304a4b356678a90aec285830b5adb8
--- /dev/null
+++ b/cypress/integration/examples/files.spec.js
@@ -0,0 +1,89 @@
+/// <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
+    // @ts-ignore
+    cy.wrap(this.example, 'fixture vs require')
+      .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/cypress/integration/examples/local_storage.spec.js b/cypress/integration/examples/local_storage.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5f83b8de73d7cfaa7e3f94ef6989a27e2828fac3
--- /dev/null
+++ b/cypress/integration/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
+    })
+
+    // Clear key matching string in Local Storage
+    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')
+    })
+
+    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')
+    })
+
+    // Clear keys matching regex in Local Storage
+    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')
+    })
+
+    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/cypress/integration/examples/location.spec.js b/cypress/integration/examples/location.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..299867da07ef0e82bd055098f01558d9e4230fa4
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/misc.spec.js b/cypress/integration/examples/misc.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..7222bf4bd1c99cdafaed24057497182737c3fd78
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/navigation.spec.js b/cypress/integration/examples/navigation.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b85a46890c8f946ac3d9b28bb4da4dbe56ea1a89
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/network_requests.spec.js b/cypress/integration/examples/network_requests.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..11213a0e852a4e3515b8b508558f1aea652be904
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/querying.spec.js b/cypress/integration/examples/querying.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..00970480f6c8641dd2ee12a2afb417e720417454
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/spies_stubs_clocks.spec.js b/cypress/integration/examples/spies_stubs_clocks.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..18b643ecd5a50b66edf26303a5307df98e918c17
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/traversal.spec.js b/cypress/integration/examples/traversal.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a3b9d33062dacdd49377d462641853ebdf3ba08
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/utilities.spec.js b/cypress/integration/examples/utilities.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..24e61a6a7c4d1436f66236883603a5ddd16d6597
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/viewport.spec.js b/cypress/integration/examples/viewport.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbcd7eeddd01e0c8f6b79ed2e8ebe69f67ce666d
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/waiting.spec.js b/cypress/integration/examples/waiting.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8f0d7c6723d27522415288acc22654c4752f694
--- /dev/null
+++ b/cypress/integration/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/cypress/integration/examples/window.spec.js b/cypress/integration/examples/window.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f94b64971db16f7f69b38958a77897b321ffeec8
--- /dev/null
+++ b/cypress/integration/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/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..59b2bab6e4e6059f0bd4f1ff986f8a9a2f381af9
--- /dev/null
+++ b/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/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000000000000000000000000000000000000..119ab03f7cda1f826e896639924a8612d075d3fc
--- /dev/null
+++ b/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/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d68db96df2697e0835f5c490db0c2cc81673f407
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// 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')
diff --git a/functions/index.js b/functions/index.js
index 6fe86d6add8f3b6808baf4f381cf57491c2904d1..31e9f5aa2af8bbf998edcbe6ebfd5591d07ef46c 100644
--- a/functions/index.js
+++ b/functions/index.js
@@ -7,7 +7,7 @@ exports.deleteOldSignals = functions.pubsub
     .schedule("* * * * *").onRun(() => {
       const ref = admin.database().ref("signals"); // reference to the items
       const now = Date.now();
-      const cutoff = now - 2 * 60 * 1000; // 2m
+      const cutoff = now - 10 * 60 * 1000; // 10m
       const oldItemsQuery = ref.orderByChild("timestamp").endAt(cutoff);
       return oldItemsQuery.once("value", function(snapshot) {
         // create a map with all children that need to be removed
diff --git a/package-lock.json b/package-lock.json
index c34bddc1c8365d27fa788366bfff4b846a32d7cb..b907aedac4f5f5aca45692bfa01b78afe6f13fd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1142,6 +1142,146 @@
         "to-fast-properties": "^2.0.0"
       }
     },
+    "@cypress/listr-verbose-renderer": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
+      "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "cli-cursor": "^1.0.2",
+        "date-fns": "^1.27.2",
+        "figures": "^1.7.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+          "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^1.0.1"
+          }
+        },
+        "figures": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+          "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5",
+            "object-assign": "^4.1.0"
+          }
+        },
+        "onetime": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+          "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+          "dev": true
+        },
+        "restore-cursor": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+          "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+          "dev": true,
+          "requires": {
+            "exit-hook": "^1.0.0",
+            "onetime": "^1.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        }
+      }
+    },
+    "@cypress/request": {
+      "version": "2.88.5",
+      "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz",
+      "integrity": "sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==",
+      "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",
+        "har-validator": "~5.1.3",
+        "http-signature": "~1.2.0",
+        "is-typedarray": "~1.0.0",
+        "isstream": "~0.1.2",
+        "json-stringify-safe": "~5.0.1",
+        "mime-types": "~2.1.19",
+        "oauth-sign": "~0.9.0",
+        "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": "^3.3.2"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+          "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"
+          }
+        }
+      }
+    },
     "@electron/get": {
       "version": "1.12.4",
       "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.12.4.tgz",
@@ -1746,6 +1886,15 @@
         "hash-sum": "^2.0.0"
       }
     },
+    "@samverschueren/stream-to-observable": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz",
+      "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==",
+      "dev": true,
+      "requires": {
+        "any-observable": "^0.3.0"
+      }
+    },
     "@sindresorhus/is": {
       "version": "0.14.0",
       "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -1951,6 +2100,18 @@
         "@types/node": "*"
       }
     },
+    "@types/sinonjs__fake-timers": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
+      "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==",
+      "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/source-list-map": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -2502,6 +2663,12 @@
         "color-convert": "^1.9.0"
       }
     },
+    "any-observable": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
+      "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
+      "dev": true
+    },
     "anymatch": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -2518,6 +2685,12 @@
       "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
       "dev": true
     },
+    "arch": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+      "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+      "dev": true
+    },
     "archiver": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz",
@@ -2638,6 +2811,15 @@
       "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
       "dev": true
     },
+    "asn1": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
     "asn1.js": {
       "version": "5.4.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
@@ -2691,6 +2873,12 @@
       "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==",
       "dev": true
     },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -2721,6 +2909,12 @@
       "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
       "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",
@@ -2798,6 +2992,18 @@
         }
       }
     },
+    "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
+    },
     "axios": {
       "version": "0.21.1",
       "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
@@ -2973,6 +3179,15 @@
       "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
       "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.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -3000,6 +3215,12 @@
         "readable-stream": "^3.4.0"
       }
     },
+    "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",
@@ -3348,6 +3569,12 @@
         }
       }
     },
+    "cachedir": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+      "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+      "dev": true
+    },
     "call-bind": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@@ -3416,6 +3643,12 @@
       "integrity": "sha512-iDUOH+oFeBYk5XawYsPtsx/8fFpndAPUQJC7gBTfxHM8xw5nOZv7ceAD4frS1MKCLUac7QL5wdAJiFQlDRjXlA==",
       "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": "4.1.0",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
@@ -3482,6 +3715,12 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
+    "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
+    },
     "chokidar": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
@@ -3592,6 +3831,90 @@
         "restore-cursor": "^3.1.0"
       }
     },
+    "cli-table3": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz",
+      "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==",
+      "dev": true,
+      "requires": {
+        "colors": "^1.1.2",
+        "object-assign": "^4.1.0",
+        "string-width": "^4.2.0"
+      },
+      "dependencies": {
+        "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
+        },
+        "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
+        },
+        "string-width": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
+          "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^8.0.0",
+            "is-fullwidth-code-point": "^3.0.0",
+            "strip-ansi": "^6.0.0"
+          }
+        }
+      }
+    },
+    "cli-truncate": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
+      "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
+      "dev": true,
+      "requires": {
+        "slice-ansi": "0.0.4",
+        "string-width": "^1.0.1"
+      },
+      "dependencies": {
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+          "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+          "dev": true,
+          "requires": {
+            "number-is-nan": "^1.0.0"
+          }
+        },
+        "slice-ansi": {
+          "version": "0.0.4",
+          "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+          "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+          "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+          "dev": true,
+          "requires": {
+            "code-point-at": "^1.0.0",
+            "is-fullwidth-code-point": "^1.0.0",
+            "strip-ansi": "^3.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        }
+      }
+    },
     "cli-width": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
@@ -3687,6 +4010,12 @@
         }
       }
     },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
     "collection-visit": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -3744,12 +4073,27 @@
       "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=",
       "dev": 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": "2.20.3",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
       "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
       "dev": true
     },
+    "common-tags": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
+      "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
+      "dev": true
+    },
     "commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -4709,6 +5053,156 @@
       "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
       "dev": true
     },
+    "cypress": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/cypress/-/cypress-7.2.0.tgz",
+      "integrity": "sha512-lHHGay+YsffDn4M0bkkwezylBVHUpwwhtqte4LNPrFRCHy77X38+1PUe3neFb3glVTM+rbILtTN6FhO2djcOuQ==",
+      "dev": true,
+      "requires": {
+        "@cypress/listr-verbose-renderer": "^0.4.1",
+        "@cypress/request": "^2.88.5",
+        "@cypress/xvfb": "^1.2.4",
+        "@types/node": "^14.14.31",
+        "@types/sinonjs__fake-timers": "^6.0.2",
+        "@types/sizzle": "^2.3.2",
+        "arch": "^2.2.0",
+        "blob-util": "^2.0.2",
+        "bluebird": "^3.7.2",
+        "cachedir": "^2.3.0",
+        "chalk": "^4.1.0",
+        "check-more-types": "^2.24.0",
+        "cli-table3": "~0.6.0",
+        "commander": "^5.1.0",
+        "common-tags": "^1.8.0",
+        "dayjs": "^1.10.4",
+        "debug": "4.3.2",
+        "eventemitter2": "^6.4.3",
+        "execa": "4.1.0",
+        "executable": "^4.1.1",
+        "extract-zip": "^1.7.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",
+        "listr": "^0.14.3",
+        "lodash": "^4.17.21",
+        "log-symbols": "^4.0.0",
+        "minimist": "^1.2.5",
+        "ospath": "^1.2.2",
+        "pretty-bytes": "^5.6.0",
+        "ramda": "~0.27.1",
+        "request-progress": "^3.0.0",
+        "supports-color": "^8.1.1",
+        "tmp": "~0.2.1",
+        "untildify": "^4.0.0",
+        "url": "^0.11.0",
+        "yauzl": "^2.10.0"
+      },
+      "dependencies": {
+        "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
+        },
+        "debug": {
+          "version": "4.3.2",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+          "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+          "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"
+          }
+        },
+        "get-stream": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+          "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+          "dev": true,
+          "requires": {
+            "pump": "^3.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
+        },
+        "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
+        },
+        "npm-run-path": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+          "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+          "dev": true,
+          "requires": {
+            "path-key": "^3.0.0"
+          }
+        },
+        "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"
+          }
+        },
+        "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"
+          }
+        }
+      }
+    },
+    "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"
+      }
+    },
+    "date-fns": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
+      "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
+      "dev": true
+    },
+    "dayjs": {
+      "version": "1.10.4",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
+      "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==",
+      "dev": true
+    },
     "debug": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -4898,6 +5392,12 @@
         }
       }
     },
+    "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",
@@ -5138,6 +5638,16 @@
         }
       }
     },
+    "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"
+      }
+    },
     "ecdsa-sig-formatter": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -5227,6 +5737,12 @@
       "integrity": "sha512-E5ha1pE9+aWWrT2fUD5wdPBWUnYtKnEnloewbtVyrkAs79HvodOiNO4rMR94+hKbxgMFQG4fnPQACOc1cfMfBg==",
       "dev": true
     },
+    "elegant-spinner": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
+      "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
+      "dev": true
+    },
     "elementtree": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz",
@@ -5695,6 +6211,12 @@
       "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
       "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
     },
+    "eventemitter2": {
+      "version": "6.4.4",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz",
+      "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==",
+      "dev": true
+    },
     "eventemitter3": {
       "version": "4.0.7",
       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -5792,6 +6314,29 @@
         }
       }
     },
+    "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
+        }
+      }
+    },
+    "exit-hook": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
+      "dev": true
+    },
     "exit-on-epipe": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
@@ -6005,6 +6550,50 @@
         }
       }
     },
+    "extract-zip": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
+      "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
+      "dev": true,
+      "requires": {
+        "concat-stream": "^1.6.2",
+        "debug": "^2.6.9",
+        "mkdirp": "^0.5.4",
+        "yauzl": "^2.10.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "mkdirp": {
+          "version": "0.5.5",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+          "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.5"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -6059,6 +6648,15 @@
         "websocket-driver": ">=0.5.1"
       }
     },
+    "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"
+      }
+    },
     "figgy-pudding": {
       "version": "3.5.2",
       "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz",
@@ -6302,6 +6900,12 @@
       "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
       "dev": true
     },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
     "fork-ts-checker-webpack-plugin": {
       "version": "6.1.0",
       "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.1.0.tgz",
@@ -6376,6 +6980,17 @@
         }
       }
     },
+    "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",
@@ -6657,6 +7272,24 @@
       "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
       "dev": true
     },
+    "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",
@@ -6696,6 +7329,23 @@
         "serialize-error": "^7.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"
+      },
+      "dependencies": {
+        "ini": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
+          "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
+          "dev": true
+        }
+      }
+    },
     "global-tunnel-ng": {
       "version": "2.7.1",
       "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz",
@@ -6813,6 +7463,22 @@
       "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
       "dev": true
     },
+    "har-schema": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
+      "dev": true
+    },
+    "har-validator": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
+      "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.3",
+        "har-schema": "^2.0.0"
+      }
+    },
     "has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -7287,6 +7953,17 @@
         }
       }
     },
+    "http-signature": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "^1.0.0",
+        "jsprim": "^1.2.2",
+        "sshpk": "^1.7.0"
+      }
+    },
     "https-browserify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -7302,6 +7979,12 @@
         "debug": "4"
       }
     },
+    "human-signals": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+      "dev": true
+    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -7621,6 +8304,23 @@
       "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==",
       "dev": true
     },
+    "is-ci": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz",
+      "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==",
+      "dev": true,
+      "requires": {
+        "ci-info": "^3.1.1"
+      },
+      "dependencies": {
+        "ci-info": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.1.1.tgz",
+          "integrity": "sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ==",
+          "dev": true
+        }
+      }
+    },
     "is-color-stop": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
@@ -7746,6 +8446,24 @@
         "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"
+      },
+      "dependencies": {
+        "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-negative-zero": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
@@ -7770,6 +8488,15 @@
       "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
       "dev": true
     },
+    "is-observable": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
+      "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
+      "dev": true,
+      "requires": {
+        "symbol-observable": "^1.1.0"
+      }
+    },
     "is-path-cwd": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@@ -7861,6 +8588,18 @@
         "has-symbols": "^1.0.1"
       }
     },
+    "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-windows": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@@ -7900,6 +8639,12 @@
       "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
       "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
+    },
     "javascript-stringify": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.0.1.tgz",
@@ -7956,6 +8701,12 @@
         "esprima": "^4.0.0"
       }
     },
+    "jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+      "dev": true
+    },
     "jsesc": {
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -7988,6 +8739,12 @@
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "dev": true
     },
+    "json-schema": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
+      "dev": true
+    },
     "json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -8004,8 +8761,7 @@
       "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,
-      "optional": true
+      "dev": true
     },
     "json3": {
       "version": "3.3.3",
@@ -8031,6 +8787,18 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "jsprim": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0",
+        "extsprintf": "1.3.0",
+        "json-schema": "0.2.3",
+        "verror": "1.10.0"
+      }
+    },
     "jstransformer": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
@@ -8129,6 +8897,12 @@
         "launch-editor": "^2.2.1"
       }
     },
+    "lazy-ass": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+      "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=",
+      "dev": true
+    },
     "lazystream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
@@ -8171,6 +8945,260 @@
       "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
       "dev": true
     },
+    "listr": {
+      "version": "0.14.3",
+      "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz",
+      "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==",
+      "dev": true,
+      "requires": {
+        "@samverschueren/stream-to-observable": "^0.3.0",
+        "is-observable": "^1.1.0",
+        "is-promise": "^2.1.0",
+        "is-stream": "^1.1.0",
+        "listr-silent-renderer": "^1.1.1",
+        "listr-update-renderer": "^0.5.0",
+        "listr-verbose-renderer": "^0.5.0",
+        "p-map": "^2.0.0",
+        "rxjs": "^6.3.3"
+      },
+      "dependencies": {
+        "p-map": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+          "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+          "dev": true
+        }
+      }
+    },
+    "listr-silent-renderer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
+      "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
+      "dev": true
+    },
+    "listr-update-renderer": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz",
+      "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==",
+      "dev": true,
+      "requires": {
+        "chalk": "^1.1.3",
+        "cli-truncate": "^0.2.1",
+        "elegant-spinner": "^1.0.1",
+        "figures": "^1.7.0",
+        "indent-string": "^3.0.0",
+        "log-symbols": "^1.0.2",
+        "log-update": "^2.3.0",
+        "strip-ansi": "^3.0.1"
+      },
+      "dependencies": {
+        "ansi-escapes": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
+          "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "figures": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+          "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5",
+            "object-assign": "^4.1.0"
+          }
+        },
+        "indent-string": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+          "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
+          "dev": true
+        },
+        "log-symbols": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+          "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+          "dev": true,
+          "requires": {
+            "chalk": "^1.0.0"
+          }
+        },
+        "log-update": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz",
+          "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "^3.0.0",
+            "cli-cursor": "^2.0.0",
+            "wrap-ansi": "^3.0.1"
+          }
+        },
+        "mimic-fn": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+          "dev": true
+        },
+        "wrap-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz",
+          "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=",
+          "dev": true,
+          "requires": {
+            "string-width": "^2.1.1",
+            "strip-ansi": "^4.0.0"
+          },
+          "dependencies": {
+            "ansi-regex": {
+              "version": "3.0.0",
+              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+              "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "4.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+              "dev": true,
+              "requires": {
+                "ansi-regex": "^3.0.0"
+              }
+            }
+          }
+        }
+      }
+    },
+    "listr-verbose-renderer": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz",
+      "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.4.1",
+        "cli-cursor": "^2.1.0",
+        "date-fns": "^1.27.2",
+        "figures": "^2.0.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "^2.0.0"
+          }
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "^1.0.5"
+          }
+        },
+        "mimic-fn": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+          "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "^1.0.0"
+          }
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "^2.0.0",
+            "signal-exit": "^3.0.2"
+          }
+        }
+      }
+    },
     "loader-runner": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -8259,6 +9287,12 @@
       "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
       "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
+    },
     "lodash.template": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
@@ -8296,6 +9330,16 @@
       "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=",
       "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"
+      }
+    },
     "log-update": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
@@ -9055,6 +10099,18 @@
       "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=",
       "dev": true
     },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "oauth-sign": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
+      "dev": true
+    },
     "object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -9284,6 +10340,12 @@
       "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
       "dev": true
     },
+    "ospath": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+      "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
+      "dev": true
+    },
     "ouch": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/ouch/-/ouch-2.0.0.tgz",
@@ -9543,6 +10605,18 @@
         "sha.js": "^2.4.8"
       }
     },
+    "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",
@@ -11714,6 +12788,12 @@
       "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
       "dev": true
     },
+    "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
+    },
     "pretty-error": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz",
@@ -11818,6 +12898,12 @@
       "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
       "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
+    },
     "public-encrypt": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -12062,6 +13148,12 @@
       "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==",
       "dev": true
     },
+    "ramda": {
+      "version": "0.27.1",
+      "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
+      "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==",
+      "dev": true
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -12275,6 +13367,15 @@
       "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
       "dev": true
     },
+    "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"
+      }
+    },
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -13187,6 +14288,23 @@
       "dev": true,
       "optional": true
     },
+    "sshpk": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+      "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+      "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"
+      }
+    },
     "ssri": {
       "version": "8.0.1",
       "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -13399,6 +14517,12 @@
       "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
       "dev": true
     },
+    "strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true
+    },
     "strip-json-comments": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -13538,6 +14662,12 @@
         }
       }
     },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+      "dev": true
+    },
     "table": {
       "version": "6.0.7",
       "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
@@ -13698,6 +14828,12 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
+    "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",
@@ -13838,6 +14974,16 @@
       "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==",
       "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"
+      }
+    },
     "ts-loader": {
       "version": "8.0.17",
       "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.17.tgz",
@@ -13882,6 +15028,21 @@
       "dev": true,
       "optional": 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-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -14066,6 +15227,12 @@
         }
       }
     },
+    "untildify": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+      "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+      "dev": true
+    },
     "upath": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -14263,6 +15430,17 @@
       "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==",
       "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"
+      }
+    },
     "vm-browserify": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -15840,6 +17018,16 @@
         "decamelize": "^1.2.0"
       }
     },
+    "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"
+      }
+    },
     "yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 3a4740e65fc7612812485929e0f840ba1a4fe93e..e39ce5e41fedd5598f5f10df1215e23209e8f9c9 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "group23_com2027",
   "version": "0.0.1",
   "description": "Enhance your nightlife experience ",
-  "productName": "Nightlife App",
+  "productName": "NightLyfe",
   "author": "Group 23",
   "private": true,
   "scripts": {
@@ -20,6 +20,7 @@
   "devDependencies": {
     "@quasar/app": "^3.0.0-beta.1",
     "babel-eslint": "^10.0.1",
+    "cypress": "^7.2.0",
     "eslint": "^7.14.0",
     "eslint-config-prettier": "^6.15.0",
     "eslint-plugin-vue": "^7.0.0",
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 7ace6d1953b07433b2472c49705ed7dfed5876a1..0000000000000000000000000000000000000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/icons/favicon-128x128.png b/public/icons/favicon-128x128.png
deleted file mode 100644
index 2d6e88c25a81b66e9ac0e5436eee56214e6cab62..0000000000000000000000000000000000000000
Binary files a/public/icons/favicon-128x128.png and /dev/null differ
diff --git a/public/icons/favicon-16x16.png b/public/icons/favicon-16x16.png
index 436be167f98f402a129b26fdcdc21fc8bdcbd271..181b8fb5685f290aaa12dcb8fa112cb851c8e8da 100644
Binary files a/public/icons/favicon-16x16.png and b/public/icons/favicon-16x16.png differ
diff --git a/public/icons/favicon-192x192.png b/public/icons/favicon-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fd65cc9743a37f9a943de8629a24fddc1d87fec
Binary files /dev/null and b/public/icons/favicon-192x192.png differ
diff --git a/public/icons/favicon-32x32.png b/public/icons/favicon-32x32.png
index b605aceb15477cf84e916b12bc1e8d9310a70fd7..e835cc84785cde25d7139229132b19cef559081b 100644
Binary files a/public/icons/favicon-32x32.png and b/public/icons/favicon-32x32.png differ
diff --git a/public/icons/favicon-512x512.png b/public/icons/favicon-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b81e0863addea78afa8afa65e4a5b506b990d9a
Binary files /dev/null and b/public/icons/favicon-512x512.png differ
diff --git a/public/icons/favicon-96x96.png b/public/icons/favicon-96x96.png
deleted file mode 100644
index 3a50144db88079f41dba7606afe161a6e11ea627..0000000000000000000000000000000000000000
Binary files a/public/icons/favicon-96x96.png and /dev/null differ
diff --git a/public/icons/favicon.ico b/public/icons/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..1ce94f9c5856fce8491432c1b988f4fa3a771664
Binary files /dev/null and b/public/icons/favicon.ico differ
diff --git a/quasar.conf.js b/quasar.conf.js
index 70c88ed16e5d784c02ec62335237aed540a4fd72..927ed29f185d1f0dfef23452d211e653267b8a46 100644
--- a/quasar.conf.js
+++ b/quasar.conf.js
@@ -122,8 +122,8 @@ module.exports = configure(function (/* ctx */) {
       },
 
       manifest: {
-        name: `Nightlife App`,
-        short_name: `Nightlife App`,
+        name: `NightLyfe`,
+        short_name: `NightLyfe`,
         description: `Enhance your nightlife experience `,
         display: 'standalone',
         orientation: 'portrait',
diff --git a/src-cordova/config.xml b/src-cordova/config.xml
index a94b7a93af769e157f2b1ac33f01f0faccdf85fa..8fe2bbcf8d3ada91db72d4d13f46a09f20274aeb 100644
--- a/src-cordova/config.xml
+++ b/src-cordova/config.xml
@@ -1,6 +1,6 @@
 <?xml version='1.0' encoding='utf-8'?>
 <widget id="com.group23.com2027" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
-    <name>Nightlife App</name>
+    <name>NightLyfe</name>
     <description>Enhance your nightlife experience </description>
     <author email="dev@cordova.apache.org" href="http://cordova.io">
         Apache Cordova Team
diff --git a/src-cordova/package.json b/src-cordova/package.json
index 60b5eaaeea3dd2e1167835783b25b8f41c086ac4..4974657e79b43198b3d735db7f28495db3e31767 100644
--- a/src-cordova/package.json
+++ b/src-cordova/package.json
@@ -1,6 +1,6 @@
 {
   "name": "com.group23.com2027",
-  "displayName": "Nightlife App",
+  "displayName": "NightLyfe",
   "version": "1.0.0",
   "description": "A sample Apache Cordova application that responds to the deviceready event.",
   "main": "index.js",
diff --git a/src/boot/firebase.js b/src/boot/firebase.js
index a9b7fd56290900bf469cf3413a7b28e63e42a55d..a3cb7df35754c4b0a332b506040af3637e931027 100644
--- a/src/boot/firebase.js
+++ b/src/boot/firebase.js
@@ -13,7 +13,7 @@ import "firebase/database";
 // Your web app's Firebase configuration
 // For Firebase JS SDK v7.20.0 and later, measurementId is optional
 var firebaseConfig = {
-  apiKey: "AIzaSyB9Dav9F3qIlHcu9s4zuYbkt5mYBdrHJws",
+  apiKey: "AIzaSyDWdpBzKsJwwTYInyEM7uH-pH7GNgDy2fs",
   authDomain: "nightlyfe-117a4.firebaseapp.com",
   projectId: "nightlyfe-117a4",
   storageBucket: "nightlyfe-117a4.appspot.com",
diff --git a/src/components/EventForm.vue b/src/components/EventForm.vue
index cb088f9ca5e9f94a84912e050584c350d4b10953..29cfc576e39b533d304a607bf03c16eb5e2ad88b 100644
--- a/src/components/EventForm.vue
+++ b/src/components/EventForm.vue
@@ -4,7 +4,7 @@ q-card.full-width
   q-card-section.bg-blue-grey-1(style='padding: 8px')
     span.text-subtitle1.text-weight-light.text-blue-grey-10.q-ml-sm Create your event
     q-chip.float-right(
-      @click='submitEvent()',
+      @click='validateEvent()',
       clickable,
       color='primary',
       text-color='white',
@@ -12,7 +12,7 @@ q-card.full-width
       style='margin-top: -1px'
     ) Submit Event
   //- 'Create Event' Form
-  q-form.q-pa-md(ref='eventForm')
+  q-form.q-pa-md(ref='eventForm', data-cy='eventForm')
     //- Event name field
     .row.q-pb-md
       q-input.full-width(
@@ -82,7 +82,6 @@ q-card.full-width
         v-model='eventData.friends',
         :dense='true',
         :options='friendsOptions',
-        lazy-rules,
         rounded,
         outlined,
         multiple,
@@ -116,7 +115,6 @@ q-card.full-width
 
 <script>
 import { mapState, mapActions } from 'vuex'
-import axios from 'axios'
 
 export default {
   emits: ['submitted'],
@@ -139,20 +137,29 @@ export default {
   },
 
   methods: {
-    submitEvent() {
-      this.$refs.eventForm.validate().then(async success => {
+    validateEvent() {
+      this.$refs.eventForm.validate().then(success => {
         if (success) {
           // Turn place name into coordinates to save in db
-          await this.geocodeLocation()
-          this.eventData['friends'] = this.friendsObject // turn array into object
-          this.eventData['timestamp'] = Date.now() // add timestamp
-          // Save eventData object under events node in db
-          this.firebaseSubmitEvent(this.eventData)
-          this.$emit('submitted')
+          this.geocodeLocation()
         }
       })
     },
 
+    submitEvent(results, status) {
+      if (status == 'OK') {
+        this.eventData['lat'] = results[0].geometry.location.lat()
+        this.eventData['lng'] = results[0].geometry.location.lng()
+        this.eventData['friends'] = this.friendsObject // turn array into object
+        this.eventData['timestamp'] = Date.now() // add timestamp
+        // Save eventData object under events node in db
+        this.firebaseSubmitEvent(this.eventData)
+        this.$emit('submitted')
+      } else {
+        console.log('Geocode was not successful for the following reason: ' + status)
+      }
+    },
+
     placesGetPredictions() {
       this.service.getPlacePredictions(
         {
@@ -172,20 +179,9 @@ export default {
       this.searchResults = predictions
     },
 
-    async geocodeLocation() {
-      const URL = `https://secret-ocean-49799.herokuapp.com/https://maps.googleapis.com/maps/api/place/details/json?place_id=${this.location}&fields=geometry&key=AIzaSyB9Dav9F3qIlHcu9s4zuYbkt5mYBdrHJws`
-
-      await axios
-        .get(URL)
-        .then(response => {
-          let placeLocation = response.data.result.geometry.location
-
-          this.eventData['lat'] = placeLocation.lat
-          this.eventData['lng'] = placeLocation.lng
-        })
-        .catch(error => {
-          console.log(error)
-        })
+    geocodeLocation() {
+      var geocoder = new google.maps.Geocoder()
+      geocoder.geocode({ placeId: this.location }, this.submitEvent)
     },
 
     getEventIcon(eventType) {
diff --git a/src/components/GoogleMap.vue b/src/components/GoogleMap.vue
index a2a4d9f65aee6dd133942c8a3085ffb9d6cb2699..700e3864032917858b4ea8f4ac862f0fb4b10a27 100644
--- a/src/components/GoogleMap.vue
+++ b/src/components/GoogleMap.vue
@@ -32,11 +32,11 @@ export default {
 
   data() {
     return {
+      service: null,
       toggle: false,
       map: null,
       positionMarker: null,
       heatmap: null,
-      places: [],
       crimes: []
     }
   },
@@ -88,30 +88,21 @@ export default {
           PLACES API
     **********************/
     /* Finds 20 places related to keyword 'nightlife' in 1500m proximity of center coordinates */
-    async findPlaces() {
-      const URL = `https://secret-ocean-49799.herokuapp.com/https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${this.center.lat},${this.center.lng}
-                    &radius=1500
-                    &keyword=nightlife
-                    &key=AIzaSyB9Dav9F3qIlHcu9s4zuYbkt5mYBdrHJws`
-      await axios
-        .get(URL)
-        .then(response => {
-          this.places = response.data.results
-        })
-        .catch(error => {
-          console.log(error.message)
-        })
+    initPlaces() {
+      var request = {
+        location: new google.maps.LatLng(this.center.lat, this.center.lng),
+        radius: 1500,
+        keyword: 'nightlife'
+      }
+      this.service.nearbySearch(request, this.mapsAddPlacesMarkers)
     },
 
     /* Return hmtl img element of the first picture listed on maps for an establishment */
-    getPlaceImage(place) {
-      if (place.photos) {
-        const URL =
-          `https://secret-ocean-49799.herokuapp.com/https://maps.googleapis.com/maps/api/place/photo?maxwidth=120&key=AIzaSyB9Dav9F3qIlHcu9s4zuYbkt5mYBdrHJws&photoreference=` +
-          place.photos[0].photo_reference
-        return `<img src="${URL}" height="80" width="60">`
+    getPlaceImage(placeImage) {
+      if (!placeImage) {
+        return ''
       }
-      return ''
+      return `<img width="80" height="120" src="${placeImage[0].getUrl({ maxWidth: 80, maxHeight: 120 })}">`
     },
 
     /* Return specific icon based on first element of 'types' attribute for an establishment */
@@ -126,18 +117,6 @@ export default {
       return icon
     },
 
-    /* Return a different html element based on whether an establishment is currently open or closed */
-    isPlaceOpen(place) {
-      if (place.opening_hours) {
-        let htmlElement = `<p class="text-negative">Closed</p>`
-        if (place.opening_hours.open_now) {
-          htmlElement = `<p class="text-positive">Open</p>`
-        }
-        return htmlElement
-      }
-      return ''
-    },
-
     /*********************
           CRIMES API 
     **********************/
@@ -200,6 +179,8 @@ export default {
         disableDefaultUI: true
       })
 
+      this.service = new google.maps.places.PlacesService(map)
+
       // Initializes your position on the map
       this.positionMarker = new google.maps.Marker({
         position: new google.maps.LatLng(this.center.lat, this.center.lng),
@@ -210,9 +191,7 @@ export default {
       this.map = map
 
       // Wait for google Places API to be finished fetching relevant data
-      await this.findPlaces()
-      // Mark all relevant places on the map
-      this.mapsInitPlaces()
+      this.initPlaces()
 
       // Wait for police.uk API to return crime data
       await this.getCrimeData()
@@ -235,18 +214,17 @@ export default {
 
       // Slow down!!!
       if (!this.signals) {
-        await this.sleep(100)
+        await this.sleep(200)
       }
+
       // Add all signals saved in the store on the map
-      for (const i in this.signals) {
-        let signal = this.signals[i]
-        this.mapsAddSignal(signal)
+      for (const signalId in this.signals) {
+        this.mapsAddSignal(signalId)
       }
 
       // Add all event saved in the store on the map
-      for (const i in this.events) {
-        let event = this.events[i]
-        this.mapsAddEvent(event)
+      for (const eventId in this.events) {
+        this.mapsAddEvent(eventId)
       }
 
       // Add all users saved in the store on the map
@@ -259,12 +237,16 @@ export default {
     },
 
     /* Creates markers and infoWindows for every relevant nightlife establishment in the area */
-    mapsInitPlaces() {
+    mapsAddPlacesMarkers(results, status) {
+      if (status != google.maps.places.PlacesServiceStatus.OK) {
+        return
+      }
+
       var infoWindow = new google.maps.InfoWindow()
 
-      this.places.forEach(place => {
-        const lat = place.geometry.location.lat
-        const lng = place.geometry.location.lng
+      results.forEach(place => {
+        const lat = place.geometry.location.lat()
+        const lng = place.geometry.location.lng()
 
         let marker = new google.maps.Marker({
           position: new google.maps.LatLng(lat, lng),
@@ -274,7 +256,7 @@ export default {
 
         google.maps.event.addListener(marker, 'click', () => {
           infoWindow.setContent(
-            `<div class="row nowrap"><div class="col"> ${this.getPlaceImage(place)} ${this.isPlaceOpen(place)}</div>
+            `<div class="row nowrap"><div class="col"> ${this.getPlaceImage(place.photos)} </div>
             <div class="col-7"> <p class="text-subtitle2">${place.name}</p>
             <p class="text-weight-light">${place.vicinity}</p> <p>${place.rating} ⭐</p></div></div>`
           )
diff --git a/src/components/SignalForm.vue b/src/components/SignalForm.vue
index 6b75daa3adbded4e84507d11b389fd517aa9c991..24f7a070d99fcfc2e180f7780733f195f9999f26 100644
--- a/src/components/SignalForm.vue
+++ b/src/components/SignalForm.vue
@@ -1,7 +1,7 @@
 <template lang="pug">
 q-card.full-width
   //- Create Signal Form
-  q-form(@submit='submitSignal', ref='signalForm')
+  q-form(@submit='submitSignal', ref='signalForm', data-cy='signalForm')
     //- Create Signal Header
     q-card-section.bg-blue-grey-1(style='padding: 8px')
       span.text-subtitle1.text-weight-light.text-blue-grey-10.q-ml-sm Create new signal
@@ -25,7 +25,8 @@ q-card.full-width
         transition-show='flip-up',
         transition-hide='flip-down',
         rounded,
-        outlined
+        outlined,
+        data-cy='signalType'
       )
         template(v-slot:prepend)
           q-icon(:name='icon')
diff --git a/src/index.template.html b/src/index.template.html
index 3fec50b012724fa786db3c5c42068f26e34393c0..14be4db2eb772924b3977ffc266554d387a83298 100644
--- a/src/index.template.html
+++ b/src/index.template.html
@@ -16,7 +16,7 @@
     <link rel="icon" type="image/ico" href="favicon.ico">
 
     <script async
-      src="https://maps.googleapis.com/maps/api/js?libraries=places,visualization&key=AIzaSyB9Dav9F3qIlHcu9s4zuYbkt5mYBdrHJws">
+      src="https://maps.googleapis.com/maps/api/js?libraries=places,visualization&key=AIzaSyDWdpBzKsJwwTYInyEM7uH-pH7GNgDy2fs">
     </script>
   </head>
   <body>
diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue
index 51212a553165f18c634c36975f608a822dc529bb..7b3179b0bf2b4d0f7a0702d1ea93c55c0433e711 100644
--- a/src/layouts/MainLayout.vue
+++ b/src/layouts/MainLayout.vue
@@ -52,7 +52,7 @@ q-layout(view='lHh Lpr lFf')
     q-page-sticky(position='bottom-right', :offset='[18, 105]')
       q-btn(@click='rightDrawer = !rightDrawer', fab, color='primary', icon='chat')
     q-page-sticky(position='bottom', :offset='[18, 36]')
-      q-fab(vertical-actions-align='center', color='teal-14', icon='add', direction='up')
+      q-fab(vertical-actions-align='center', color='teal-14', icon='add', direction='up', data-cy='centerBtn')
         q-fab-action(@click='newEvent = true', color='orange', icon='add_location', label='New Event')
         q-fab-action(@click='newSignal = true', color='accent', icon='notification_add', label='New Signal')
 </template>