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