element', () => {
+ // https://on.cypress.io/select
+
+ // at first, no option should be selected
+ cy.get('.action-select').should('have.value', '--Select a fruit--');
+
+ // Select option(s) with matching text content
+ cy.get('.action-select').select('apples');
+ // confirm the apples were selected
+ // note that each value starts with "fr-" in our HTML
+ cy.get('.action-select').should('have.value', 'fr-apples');
+
+ cy.get('.action-select-multiple')
+ .select(['apples', 'oranges', 'bananas'])
+ // when getting multiple values, invoke "val" method first
+ .invoke('val')
+ .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']);
+
+ // Select option(s) with matching value
+ cy.get('.action-select')
+ .select('fr-bananas')
+ // can attach an assertion right away to the element
+ .should('have.value', 'fr-bananas');
+
+ cy.get('.action-select-multiple')
+ .select(['fr-apples', 'fr-oranges', 'fr-bananas'])
+ .invoke('val')
+ .should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']);
+
+ // assert the selected values include oranges
+ cy.get('.action-select-multiple')
+ .invoke('val')
+ .should('include', 'fr-oranges');
+ });
+
+ it('.scrollIntoView() - scroll an element into view', () => {
+ // https://on.cypress.io/scrollintoview
+
+ // normally all of these buttons are hidden,
+ // because they're not within
+ // the viewable area of their parent
+ // (we need to scroll to see them)
+ cy.get('#scroll-horizontal button').should('not.be.visible');
+
+ // scroll the button into view, as if the user had scrolled
+ cy.get('#scroll-horizontal button').scrollIntoView().should('be.visible');
+
+ cy.get('#scroll-vertical button').should('not.be.visible');
+
+ // Cypress handles the scroll direction needed
+ cy.get('#scroll-vertical button').scrollIntoView().should('be.visible');
+
+ cy.get('#scroll-both button').should('not.be.visible');
+
+ // Cypress knows to scroll to the right and down
+ cy.get('#scroll-both button').scrollIntoView().should('be.visible');
+ });
+
+ it('.trigger() - trigger an event on a DOM element', () => {
+ // https://on.cypress.io/trigger
+
+ // To interact with a range input (slider)
+ // we need to set its value & trigger the
+ // event to signal it changed
+
+ // Here, we invoke jQuery's val() method to set
+ // the value and trigger the 'change' event
+ cy.get('.trigger-input-range')
+ .invoke('val', 25)
+ .trigger('change')
+ .get('input[type=range]')
+ .siblings('p')
+ .should('have.text', '25');
+ });
+
+ it('cy.scrollTo() - scroll the window or element to a position', () => {
+ // https://on.cypress.io/scrollto
+
+ // You can scroll to 9 specific positions of an element:
+ // -----------------------------------
+ // | topLeft top topRight |
+ // | |
+ // | |
+ // | |
+ // | left center right |
+ // | |
+ // | |
+ // | |
+ // | bottomLeft bottom bottomRight |
+ // -----------------------------------
+
+ // if you chain .scrollTo() off of cy, we will
+ // scroll the entire window
+ cy.scrollTo('bottom');
+
+ cy.get('#scrollable-horizontal').scrollTo('right');
+
+ // or you can scroll to a specific coordinate:
+ // (x axis, y axis) in pixels
+ cy.get('#scrollable-vertical').scrollTo(250, 250);
+
+ // or you can scroll to a specific percentage
+ // of the (width, height) of the element
+ cy.get('#scrollable-both').scrollTo('75%', '25%');
+
+ // control the easing of the scroll (default is 'swing')
+ cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' });
+
+ // control the duration of the scroll (in ms)
+ cy.get('#scrollable-both').scrollTo('center', { duration: 2000 });
+ });
+});
diff --git a/cypress/integration/examples/aliasing.spec.js b/cypress/integration/examples/aliasing.spec.js
new file mode 100644
index 0000000..61a4b40
--- /dev/null
+++ b/cypress/integration/examples/aliasing.spec.js
@@ -0,0 +1,43 @@
+///
+
+context('Aliasing', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/aliasing');
+ });
+
+ it('.as() - alias a DOM element for later use', () => {
+ // https://on.cypress.io/as
+
+ // Alias a DOM element for use later
+ // We don't have to traverse to the element
+ // later in our code, we reference it with @
+
+ cy.get('.as-table')
+ .find('tbody>tr')
+ .first()
+ .find('td')
+ .first()
+ .find('button')
+ .as('firstBtn');
+
+ // when we reference the alias, we place an
+ // @ in front of its name
+ cy.get('@firstBtn').click();
+
+ cy.get('@firstBtn')
+ .should('have.class', 'btn-success')
+ .and('contain', 'Changed');
+ });
+
+ it('.as() - alias a route for later use', () => {
+ // Alias the route to wait for its response
+ cy.intercept('GET', '**/comments/*').as('getComment');
+
+ // we have code that gets a comment when
+ // the button is clicked in scripts.js
+ cy.get('.network-btn').click();
+
+ // https://on.cypress.io/wait
+ cy.wait('@getComment').its('response.statusCode').should('eq', 200);
+ });
+});
diff --git a/cypress/integration/examples/assertions.spec.js b/cypress/integration/examples/assertions.spec.js
new file mode 100644
index 0000000..7779e3e
--- /dev/null
+++ b/cypress/integration/examples/assertions.spec.js
@@ -0,0 +1,176 @@
+///
+
+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 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 element with text content matching regular expression
+ .contains('td', /column content/i)
+ .should('be.visible');
+
+ // for more information about asserting element's text
+ // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
+ });
+
+ it('.and() - chain multiple assertions together', () => {
+ // https://on.cypress.io/and
+ cy.get('.assertions-link')
+ .should('have.class', 'active')
+ .and('have.attr', 'href')
+ .and('include', 'cypress.io');
+ });
+ });
+
+ describe('Explicit Assertions', () => {
+ // https://on.cypress.io/assertions
+ it('expect - make an assertion about a specified subject', () => {
+ // We can use Chai's BDD style assertions
+ expect(true).to.be.true;
+ const o = { foo: 'bar' };
+
+ expect(o).to.equal(o);
+ expect(o).to.deep.equal({ foo: 'bar' });
+ // matching text using regular expression
+ expect('FooBar').to.match(/bar$/i);
+ });
+
+ it('pass your own callback function to should()', () => {
+ // Pass a function to should that can have any number
+ // of explicit assertions within it.
+ // The ".should(cb)" function will be retried
+ // automatically until it passes all your explicit assertions or times out.
+ cy.get('.assertions-p')
+ .find('p')
+ .should(($p) => {
+ // https://on.cypress.io/$
+ // return an array of texts from all of the p's
+ // @ts-ignore TS6133 unused variable
+ const texts = $p.map((i, el) => Cypress.$(el).text());
+
+ // jquery map returns jquery object
+ // and .get() convert this to simple array
+ const paragraphs = texts.get();
+
+ // array should have length of 3
+ expect(paragraphs, 'has 3 paragraphs').to.have.length(3);
+
+ // use second argument to expect(...) to provide clear
+ // message with each assertion
+ expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
+ 'Some text from first p',
+ 'More text from second p',
+ 'And even more text from third p',
+ ]);
+ });
+ });
+
+ it('finds element by class name regex', () => {
+ cy.get('.docs-header')
+ .find('div')
+ // .should(cb) callback function will be retried
+ .should(($div) => {
+ expect($div).to.have.length(1);
+
+ const className = $div[0].className;
+
+ expect(className).to.match(/heading-/);
+ })
+ // .then(cb) callback is not retried,
+ // it either passes or fails
+ .then(($div) => {
+ expect($div, 'text content').to.have.text('Introduction');
+ });
+ });
+
+ it('can throw any error', () => {
+ cy.get('.docs-header')
+ .find('div')
+ .should(($div) => {
+ if ($div.length !== 1) {
+ // you can throw your own errors
+ throw new Error('Did not find 1 element');
+ }
+
+ const className = $div[0].className;
+
+ if (!className.match(/heading-/)) {
+ throw new Error(`Could not find class "heading-" in ${className}`);
+ }
+ });
+ });
+
+ it('matches unknown text between two elements', () => {
+ /**
+ * Text from the first element.
+ * @type {string}
+ */
+ let text;
+
+ /**
+ * Normalizes passed text,
+ * useful before comparing text with spaces and different capitalization.
+ * @param {string} s Text to normalize
+ */
+ const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase();
+
+ cy.get('.two-elements')
+ .find('.first')
+ .then(($first) => {
+ // save text from the first element
+ text = normalizeText($first.text());
+ });
+
+ cy.get('.two-elements')
+ .find('.second')
+ .should(($div) => {
+ // we can massage text before comparing
+ const secondText = normalizeText($div.text());
+
+ expect(secondText, 'second text').to.equal(text);
+ });
+ });
+
+ it('assert - assert shape of an object', () => {
+ const person = {
+ name: 'Joe',
+ age: 20,
+ };
+
+ assert.isObject(person, 'value is object');
+ });
+
+ it('retries the should callback until assertions pass', () => {
+ cy.get('#random-number').should(($div) => {
+ const n = parseFloat($div.text());
+
+ expect(n).to.be.gte(1).and.be.lte(10);
+ });
+ });
+ });
+});
diff --git a/cypress/integration/examples/connectors.spec.js b/cypress/integration/examples/connectors.spec.js
new file mode 100644
index 0000000..cf62028
--- /dev/null
+++ b/cypress/integration/examples/connectors.spec.js
@@ -0,0 +1,96 @@
+///
+
+context('Connectors', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/connectors');
+ });
+
+ it('.each() - iterate over an array of elements', () => {
+ // https://on.cypress.io/each
+ cy.get('.connectors-each-ul>li').each(($el, index, $list) => {
+ console.log($el, index, $list);
+ });
+ });
+
+ it('.its() - get properties on the current subject', () => {
+ // https://on.cypress.io/its
+ cy.get('.connectors-its-ul>li')
+ // calls the 'length' property yielding that value
+ .its('length')
+ .should('be.gt', 2);
+ });
+
+ it('.invoke() - invoke a function on the current subject', () => {
+ // our div is hidden in our script.js
+ // $('.connectors-div').hide()
+
+ // https://on.cypress.io/invoke
+ cy.get('.connectors-div')
+ .should('be.hidden')
+ // call the jquery method 'show' on the 'div.container'
+ .invoke('show')
+ .should('be.visible');
+ });
+
+ it('.spread() - spread an array as individual args to callback function', () => {
+ // https://on.cypress.io/spread
+ const arr = ['foo', 'bar', 'baz'];
+
+ cy.wrap(arr).spread((foo, bar, baz) => {
+ expect(foo).to.eq('foo');
+ expect(bar).to.eq('bar');
+ expect(baz).to.eq('baz');
+ });
+ });
+
+ describe('.then()', () => {
+ it('invokes a callback function with the current subject', () => {
+ // https://on.cypress.io/then
+ cy.get('.connectors-list > li').then(($lis) => {
+ expect($lis, '3 items').to.have.length(3);
+ expect($lis.eq(0), 'first item').to.contain('Walk the dog');
+ expect($lis.eq(1), 'second item').to.contain('Feed the cat');
+ expect($lis.eq(2), 'third item').to.contain('Write JavaScript');
+ });
+ });
+
+ it('yields the returned value to the next command', () => {
+ cy.wrap(1)
+ .then((num) => {
+ expect(num).to.equal(1);
+
+ return 2;
+ })
+ .then((num) => {
+ expect(num).to.equal(2);
+ });
+ });
+
+ it('yields the original subject without return', () => {
+ cy.wrap(1)
+ .then((num) => {
+ expect(num).to.equal(1);
+ // note that nothing is returned from this callback
+ })
+ .then((num) => {
+ // this callback receives the original unchanged value 1
+ expect(num).to.equal(1);
+ });
+ });
+
+ it('yields the value yielded by the last Cypress command inside', () => {
+ cy.wrap(1)
+ .then((num) => {
+ expect(num).to.equal(1);
+ // note how we run a Cypress command
+ // the result yielded by this Cypress command
+ // will be passed to the second ".then"
+ cy.wrap(2);
+ })
+ .then((num) => {
+ // this callback receives the value yielded by "cy.wrap(2)"
+ expect(num).to.equal(2);
+ });
+ });
+ });
+});
diff --git a/cypress/integration/examples/cookies.spec.js b/cypress/integration/examples/cookies.spec.js
new file mode 100644
index 0000000..87b2e5b
--- /dev/null
+++ b/cypress/integration/examples/cookies.spec.js
@@ -0,0 +1,79 @@
+///
+
+context('Cookies', () => {
+ beforeEach(() => {
+ Cypress.Cookies.debug(true);
+
+ cy.visit('https://example.cypress.io/commands/cookies');
+
+ // clear cookies again after visiting to remove
+ // any 3rd party cookies picked up such as cloudflare
+ cy.clearCookies();
+ });
+
+ it('cy.getCookie() - get a browser cookie', () => {
+ // https://on.cypress.io/getcookie
+ cy.get('#getCookie .set-a-cookie').click();
+
+ // cy.getCookie() yields a cookie object
+ cy.getCookie('token').should('have.property', 'value', '123ABC');
+ });
+
+ it('cy.getCookies() - get browser cookies', () => {
+ // https://on.cypress.io/getcookies
+ cy.getCookies().should('be.empty');
+
+ cy.get('#getCookies .set-a-cookie').click();
+
+ // cy.getCookies() yields an array of cookies
+ cy.getCookies()
+ .should('have.length', 1)
+ .should((cookies) => {
+ // each cookie has these properties
+ expect(cookies[0]).to.have.property('name', 'token');
+ expect(cookies[0]).to.have.property('value', '123ABC');
+ expect(cookies[0]).to.have.property('httpOnly', false);
+ expect(cookies[0]).to.have.property('secure', false);
+ expect(cookies[0]).to.have.property('domain');
+ expect(cookies[0]).to.have.property('path');
+ });
+ });
+
+ it('cy.setCookie() - set a browser cookie', () => {
+ // https://on.cypress.io/setcookie
+ cy.getCookies().should('be.empty');
+
+ cy.setCookie('foo', 'bar');
+
+ // cy.getCookie() yields a cookie object
+ cy.getCookie('foo').should('have.property', 'value', 'bar');
+ });
+
+ it('cy.clearCookie() - clear a browser cookie', () => {
+ // https://on.cypress.io/clearcookie
+ cy.getCookie('token').should('be.null');
+
+ cy.get('#clearCookie .set-a-cookie').click();
+
+ cy.getCookie('token').should('have.property', 'value', '123ABC');
+
+ // cy.clearCookies() yields null
+ cy.clearCookie('token').should('be.null');
+
+ cy.getCookie('token').should('be.null');
+ });
+
+ it('cy.clearCookies() - clear browser cookies', () => {
+ // https://on.cypress.io/clearcookies
+ cy.getCookies().should('be.empty');
+
+ cy.get('#clearCookies .set-a-cookie').click();
+
+ cy.getCookies().should('have.length', 1);
+
+ // cy.clearCookies() yields null
+ cy.clearCookies();
+
+ cy.getCookies().should('be.empty');
+ });
+});
diff --git a/cypress/integration/examples/cypress_api.spec.js b/cypress/integration/examples/cypress_api.spec.js
new file mode 100644
index 0000000..7e642f9
--- /dev/null
+++ b/cypress/integration/examples/cypress_api.spec.js
@@ -0,0 +1,215 @@
+///
+
+context('Cypress.Commands', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ // https://on.cypress.io/custom-commands
+
+ it('.add() - create a custom command', () => {
+ Cypress.Commands.add(
+ 'console',
+ {
+ prevSubject: true,
+ },
+ (subject, method) => {
+ // the previous subject is automatically received
+ // and the commands arguments are shifted
+
+ // allow us to change the console method used
+ method = method || 'log';
+
+ // log the subject to the console
+ // @ts-ignore TS7017
+ console[method]('The subject is', subject);
+
+ // whatever we return becomes the new subject
+ // we don't want to change the subject so
+ // we return whatever was passed in
+ return subject;
+ }
+ );
+
+ // @ts-ignore TS2339
+ cy.get('button')
+ .console('info')
+ .then(($button) => {
+ // subject is still $button
+ });
+ });
+});
+
+context('Cypress.Cookies', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ // https://on.cypress.io/cookies
+ it('.debug() - enable or disable debugging', () => {
+ Cypress.Cookies.debug(true);
+
+ // Cypress will now log in the console when
+ // cookies are set or cleared
+ cy.setCookie('fakeCookie', '123ABC');
+ cy.clearCookie('fakeCookie');
+ cy.setCookie('fakeCookie', '123ABC');
+ cy.clearCookie('fakeCookie');
+ cy.setCookie('fakeCookie', '123ABC');
+ });
+
+ it('.preserveOnce() - preserve cookies by key', () => {
+ // normally cookies are reset after each test
+ cy.getCookie('fakeCookie').should('not.be.ok');
+
+ // preserving a cookie will not clear it when
+ // the next test starts
+ cy.setCookie('lastCookie', '789XYZ');
+ Cypress.Cookies.preserveOnce('lastCookie');
+ });
+
+ it('.defaults() - set defaults for all cookies', () => {
+ // now any cookie with the name 'session_id' will
+ // not be cleared before each new test runs
+ Cypress.Cookies.defaults({
+ preserve: 'session_id',
+ });
+ });
+});
+
+context('Cypress.arch', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Get CPU architecture name of underlying OS', () => {
+ // https://on.cypress.io/arch
+ expect(Cypress.arch).to.exist;
+ });
+});
+
+context('Cypress.config()', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Get and set configuration options', () => {
+ // https://on.cypress.io/config
+ let myConfig = Cypress.config();
+
+ expect(myConfig).to.have.property('animationDistanceThreshold', 5);
+ expect(myConfig).to.have.property('baseUrl', null);
+ expect(myConfig).to.have.property('defaultCommandTimeout', 4000);
+ expect(myConfig).to.have.property('requestTimeout', 5000);
+ expect(myConfig).to.have.property('responseTimeout', 30000);
+ expect(myConfig).to.have.property('viewportHeight', 660);
+ expect(myConfig).to.have.property('viewportWidth', 1000);
+ expect(myConfig).to.have.property('pageLoadTimeout', 60000);
+ expect(myConfig).to.have.property('waitForAnimations', true);
+
+ expect(Cypress.config('pageLoadTimeout')).to.eq(60000);
+
+ // this will change the config for the rest of your tests!
+ Cypress.config('pageLoadTimeout', 20000);
+
+ expect(Cypress.config('pageLoadTimeout')).to.eq(20000);
+
+ Cypress.config('pageLoadTimeout', 60000);
+ });
+});
+
+context('Cypress.dom', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ // https://on.cypress.io/dom
+ it('.isHidden() - determine if a DOM element is hidden', () => {
+ let hiddenP = Cypress.$('.dom-p p.hidden').get(0);
+ let visibleP = Cypress.$('.dom-p p.visible').get(0);
+
+ // our first paragraph has css class 'hidden'
+ expect(Cypress.dom.isHidden(hiddenP)).to.be.true;
+ expect(Cypress.dom.isHidden(visibleP)).to.be.false;
+ });
+});
+
+context('Cypress.env()', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ // We can set environment variables for highly dynamic values
+
+ // https://on.cypress.io/environment-variables
+ it('Get environment variables', () => {
+ // https://on.cypress.io/env
+ // set multiple environment variables
+ Cypress.env({
+ host: 'veronica.dev.local',
+ api_server: 'http://localhost:8888/v1/',
+ });
+
+ // get environment variable
+ expect(Cypress.env('host')).to.eq('veronica.dev.local');
+
+ // set environment variable
+ Cypress.env('api_server', 'http://localhost:8888/v2/');
+ expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/');
+
+ // get all environment variable
+ expect(Cypress.env()).to.have.property('host', 'veronica.dev.local');
+ expect(Cypress.env()).to.have.property(
+ 'api_server',
+ 'http://localhost:8888/v2/'
+ );
+ });
+});
+
+context('Cypress.log', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Control what is printed to the Command Log', () => {
+ // https://on.cypress.io/cypress-log
+ });
+});
+
+context('Cypress.platform', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Get underlying OS name', () => {
+ // https://on.cypress.io/platform
+ expect(Cypress.platform).to.be.exist;
+ });
+});
+
+context('Cypress.version', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Get current version of Cypress being run', () => {
+ // https://on.cypress.io/version
+ expect(Cypress.version).to.be.exist;
+ });
+});
+
+context('Cypress.spec', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/cypress-api');
+ });
+
+ it('Get current spec information', () => {
+ // https://on.cypress.io/spec
+ // wrap the object so we can inspect it easily by clicking in the command log
+ cy.wrap(Cypress.spec).should('include.keys', [
+ 'name',
+ 'relative',
+ 'absolute',
+ ]);
+ });
+});
diff --git a/cypress/integration/examples/files.spec.js b/cypress/integration/examples/files.spec.js
new file mode 100644
index 0000000..ede0e8a
--- /dev/null
+++ b/cypress/integration/examples/files.spec.js
@@ -0,0 +1,94 @@
+///
+
+/// JSON fixture file can be loaded directly using
+// the built-in JavaScript bundler
+// @ts-ignore
+const requiredExample = require('../../fixtures/example');
+
+context('Files', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/files');
+ });
+
+ beforeEach(() => {
+ // load example.json fixture file and store
+ // in the test context object
+ cy.fixture('example.json').as('example');
+ });
+
+ it('cy.fixture() - load a fixture', () => {
+ // https://on.cypress.io/fixture
+
+ // Instead of writing a response inline you can
+ // use a fixture file's content.
+
+ // when application makes an Ajax request matching "GET **/comments/*"
+ // Cypress will intercept it and reply with the object in `example.json` fixture
+ cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as(
+ 'getComment'
+ );
+
+ // we have code that gets a comment when
+ // the button is clicked in scripts.js
+ cy.get('.fixture-btn').click();
+
+ cy.wait('@getComment')
+ .its('response.body')
+ .should('have.property', 'name')
+ .and('include', 'Using fixtures to represent data');
+ });
+
+ it('cy.fixture() or require - load a fixture', function () {
+ // we are inside the "function () { ... }"
+ // callback and can use test context object "this"
+ // "this.example" was loaded in "beforeEach" function callback
+ expect(this.example, 'fixture in the test context').to.deep.equal(
+ requiredExample
+ );
+
+ // or use "cy.wrap" and "should('deep.equal', ...)" assertion
+ // @ts-ignore
+ cy.wrap(this.example, 'fixture vs require').should(
+ 'deep.equal',
+ requiredExample
+ );
+ });
+
+ it('cy.readFile() - read file contents', () => {
+ // https://on.cypress.io/readfile
+
+ // You can read a file and yield its contents
+ // The filePath is relative to your project's root.
+ cy.readFile('cypress.json').then((json) => {
+ expect(json).to.be.an('object');
+ });
+ });
+
+ it('cy.writeFile() - write to a file', () => {
+ // https://on.cypress.io/writefile
+
+ // You can write to a file
+
+ // Use a response from a request to automatically
+ // generate a fixture file for use later
+ cy.request('https://jsonplaceholder.cypress.io/users').then((response) => {
+ cy.writeFile('cypress/fixtures/users.json', response.body);
+ });
+
+ cy.fixture('users').should((users) => {
+ expect(users[0].name).to.exist;
+ });
+
+ // JavaScript arrays and objects are stringified
+ // and formatted into text.
+ cy.writeFile('cypress/fixtures/profile.json', {
+ id: 8739,
+ name: 'Jane',
+ email: 'jane@example.com',
+ });
+
+ cy.fixture('profile').should((profile) => {
+ expect(profile.name).to.eq('Jane');
+ });
+ });
+});
diff --git a/cypress/integration/examples/local_storage.spec.js b/cypress/integration/examples/local_storage.spec.js
new file mode 100644
index 0000000..1946c4d
--- /dev/null
+++ b/cypress/integration/examples/local_storage.spec.js
@@ -0,0 +1,58 @@
+///
+
+context('Local Storage', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/local-storage');
+ });
+ // Although local storage is automatically cleared
+ // in between tests to maintain a clean state
+ // sometimes we need to clear the local storage manually
+
+ it('cy.clearLocalStorage() - clear all data in local storage', () => {
+ // https://on.cypress.io/clearlocalstorage
+ cy.get('.ls-btn')
+ .click()
+ .should(() => {
+ expect(localStorage.getItem('prop1')).to.eq('red');
+ expect(localStorage.getItem('prop2')).to.eq('blue');
+ expect(localStorage.getItem('prop3')).to.eq('magenta');
+ });
+
+ // clearLocalStorage() yields the localStorage object
+ cy.clearLocalStorage().should((ls) => {
+ expect(ls.getItem('prop1')).to.be.null;
+ expect(ls.getItem('prop2')).to.be.null;
+ expect(ls.getItem('prop3')).to.be.null;
+ });
+
+ // Clear key matching string in Local Storage
+ cy.get('.ls-btn')
+ .click()
+ .should(() => {
+ expect(localStorage.getItem('prop1')).to.eq('red');
+ expect(localStorage.getItem('prop2')).to.eq('blue');
+ expect(localStorage.getItem('prop3')).to.eq('magenta');
+ });
+
+ cy.clearLocalStorage('prop1').should((ls) => {
+ expect(ls.getItem('prop1')).to.be.null;
+ expect(ls.getItem('prop2')).to.eq('blue');
+ expect(ls.getItem('prop3')).to.eq('magenta');
+ });
+
+ // Clear keys matching regex in Local Storage
+ cy.get('.ls-btn')
+ .click()
+ .should(() => {
+ expect(localStorage.getItem('prop1')).to.eq('red');
+ expect(localStorage.getItem('prop2')).to.eq('blue');
+ expect(localStorage.getItem('prop3')).to.eq('magenta');
+ });
+
+ cy.clearLocalStorage(/prop1|2/).should((ls) => {
+ expect(ls.getItem('prop1')).to.be.null;
+ expect(ls.getItem('prop2')).to.be.null;
+ expect(ls.getItem('prop3')).to.eq('magenta');
+ });
+ });
+});
diff --git a/cypress/integration/examples/location.spec.js b/cypress/integration/examples/location.spec.js
new file mode 100644
index 0000000..481c930
--- /dev/null
+++ b/cypress/integration/examples/location.spec.js
@@ -0,0 +1,34 @@
+///
+
+context('Location', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/location');
+ });
+
+ it('cy.hash() - get the current URL hash', () => {
+ // https://on.cypress.io/hash
+ cy.hash().should('be.empty');
+ });
+
+ it('cy.location() - get window.location', () => {
+ // https://on.cypress.io/location
+ cy.location().should((location) => {
+ expect(location.hash).to.be.empty;
+ expect(location.href).to.eq(
+ 'https://example.cypress.io/commands/location'
+ );
+ expect(location.host).to.eq('example.cypress.io');
+ expect(location.hostname).to.eq('example.cypress.io');
+ expect(location.origin).to.eq('https://example.cypress.io');
+ expect(location.pathname).to.eq('/commands/location');
+ expect(location.port).to.eq('');
+ expect(location.protocol).to.eq('https:');
+ expect(location.search).to.be.empty;
+ });
+ });
+
+ it('cy.url() - get the current URL', () => {
+ // https://on.cypress.io/url
+ cy.url().should('eq', 'https://example.cypress.io/commands/location');
+ });
+});
diff --git a/cypress/integration/examples/misc.spec.js b/cypress/integration/examples/misc.spec.js
new file mode 100644
index 0000000..2f99f4b
--- /dev/null
+++ b/cypress/integration/examples/misc.spec.js
@@ -0,0 +1,102 @@
+///
+
+context('Misc', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/misc');
+ });
+
+ it('.end() - end the command chain', () => {
+ // https://on.cypress.io/end
+
+ // cy.end is useful when you want to end a chain of commands
+ // and force Cypress to re-query from the root element
+ cy.get('.misc-table').within(() => {
+ // ends the current chain and yields null
+ cy.contains('Cheryl').click().end();
+
+ // queries the entire table again
+ cy.contains('Charles').click();
+ });
+ });
+
+ it('cy.exec() - execute a system command', () => {
+ // execute a system command.
+ // so you can take actions necessary for
+ // your test outside the scope of Cypress.
+ // https://on.cypress.io/exec
+
+ // we can use Cypress.platform string to
+ // select appropriate command
+ // https://on.cypress/io/platform
+ cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`);
+
+ // on CircleCI Windows build machines we have a failure to run bash shell
+ // https://github.com/cypress-io/cypress/issues/5169
+ // so skip some of the tests by passing flag "--env circle=true"
+ const isCircleOnWindows =
+ Cypress.platform === 'win32' && Cypress.env('circle');
+
+ if (isCircleOnWindows) {
+ cy.log('Skipping test on CircleCI');
+
+ return;
+ }
+
+ // cy.exec problem on Shippable CI
+ // https://github.com/cypress-io/cypress/issues/6718
+ const isShippable =
+ Cypress.platform === 'linux' && Cypress.env('shippable');
+
+ if (isShippable) {
+ cy.log('Skipping test on ShippableCI');
+
+ return;
+ }
+
+ cy.exec('echo Jane Lane').its('stdout').should('contain', 'Jane Lane');
+
+ if (Cypress.platform === 'win32') {
+ cy.exec('print cypress.json').its('stderr').should('be.empty');
+ } else {
+ cy.exec('cat cypress.json').its('stderr').should('be.empty');
+
+ cy.exec('pwd').its('code').should('eq', 0);
+ }
+ });
+
+ it('cy.focused() - get the DOM element that has focus', () => {
+ // https://on.cypress.io/focused
+ cy.get('.misc-form').find('#name').click();
+ cy.focused().should('have.id', 'name');
+
+ cy.get('.misc-form').find('#description').click();
+ cy.focused().should('have.id', 'description');
+ });
+
+ context('Cypress.Screenshot', function () {
+ it('cy.screenshot() - take a screenshot', () => {
+ // https://on.cypress.io/screenshot
+ cy.screenshot('my-image');
+ });
+
+ it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
+ Cypress.Screenshot.defaults({
+ blackout: ['.foo'],
+ capture: 'viewport',
+ clip: { x: 0, y: 0, width: 200, height: 200 },
+ scale: false,
+ disableTimersAndAnimations: true,
+ screenshotOnRunFailure: true,
+ onBeforeScreenshot() {},
+ onAfterScreenshot() {},
+ });
+ });
+ });
+
+ it('cy.wrap() - wrap an object', () => {
+ // https://on.cypress.io/wrap
+ cy.wrap({ foo: 'bar' })
+ .should('have.property', 'foo')
+ .and('include', 'bar');
+ });
+});
diff --git a/cypress/integration/examples/navigation.spec.js b/cypress/integration/examples/navigation.spec.js
new file mode 100644
index 0000000..8a66ac4
--- /dev/null
+++ b/cypress/integration/examples/navigation.spec.js
@@ -0,0 +1,56 @@
+///
+
+context('Navigation', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io');
+ cy.get('.navbar-nav').contains('Commands').click();
+ cy.get('.dropdown-menu').contains('Navigation').click();
+ });
+
+ it("cy.go() - go back or forward in the browser's history", () => {
+ // https://on.cypress.io/go
+
+ cy.location('pathname').should('include', 'navigation');
+
+ cy.go('back');
+ cy.location('pathname').should('not.include', 'navigation');
+
+ cy.go('forward');
+ cy.location('pathname').should('include', 'navigation');
+
+ // clicking back
+ cy.go(-1);
+ cy.location('pathname').should('not.include', 'navigation');
+
+ // clicking forward
+ cy.go(1);
+ cy.location('pathname').should('include', 'navigation');
+ });
+
+ it('cy.reload() - reload the page', () => {
+ // https://on.cypress.io/reload
+ cy.reload();
+
+ // reload the page without using the cache
+ cy.reload(true);
+ });
+
+ it('cy.visit() - visit a remote url', () => {
+ // https://on.cypress.io/visit
+
+ // Visit any sub-domain of your current domain
+
+ // Pass options to the visit
+ cy.visit('https://example.cypress.io/commands/navigation', {
+ timeout: 50000, // increase total time for the visit to resolve
+ onBeforeLoad(contentWindow) {
+ // contentWindow is the remote page's window object
+ expect(typeof contentWindow === 'object').to.be.true;
+ },
+ onLoad(contentWindow) {
+ // contentWindow is the remote page's window object
+ expect(typeof contentWindow === 'object').to.be.true;
+ },
+ });
+ });
+});
diff --git a/cypress/integration/examples/network_requests.spec.js b/cypress/integration/examples/network_requests.spec.js
new file mode 100644
index 0000000..5412f5f
--- /dev/null
+++ b/cypress/integration/examples/network_requests.spec.js
@@ -0,0 +1,186 @@
+///
+
+context('Network Requests', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/network-requests');
+ });
+
+ // Manage HTTP requests in your app
+
+ it('cy.request() - make an XHR request', () => {
+ // https://on.cypress.io/request
+ cy.request('https://jsonplaceholder.cypress.io/comments').should(
+ (response) => {
+ expect(response.status).to.eq(200);
+ // the server sometimes gets an extra comment posted from another machine
+ // which gets returned as 1 extra object
+ expect(response.body)
+ .to.have.property('length')
+ .and.be.oneOf([500, 501]);
+ expect(response).to.have.property('headers');
+ expect(response).to.have.property('duration');
+ }
+ );
+ });
+
+ it('cy.request() - verify response using BDD syntax', () => {
+ cy.request('https://jsonplaceholder.cypress.io/comments').then(
+ (response) => {
+ // https://on.cypress.io/assertions
+ expect(response).property('status').to.equal(200);
+ expect(response)
+ .property('body')
+ .to.have.property('length')
+ .and.be.oneOf([500, 501]);
+ expect(response).to.include.keys('headers', 'duration');
+ }
+ );
+ });
+
+ it('cy.request() with query parameters', () => {
+ // will execute request
+ // https://jsonplaceholder.cypress.io/comments?postId=1&id=3
+ cy.request({
+ url: 'https://jsonplaceholder.cypress.io/comments',
+ qs: {
+ postId: 1,
+ id: 3,
+ },
+ })
+ .its('body')
+ .should('be.an', 'array')
+ .and('have.length', 1)
+ .its('0') // yields first element of the array
+ .should('contain', {
+ postId: 1,
+ id: 3,
+ });
+ });
+
+ it('cy.request() - pass result to the second request', () => {
+ // first, let's find out the userId of the first user we have
+ cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
+ .its('body') // yields the response object
+ .its('0') // yields the first element of the returned list
+ // the above two commands its('body').its('0')
+ // can be written as its('body.0')
+ // if you do not care about TypeScript checks
+ .then((user) => {
+ expect(user).property('id').to.be.a('number');
+ // make a new post on behalf of the user
+ cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
+ userId: user.id,
+ title: 'Cypress Test Runner',
+ body:
+ 'Fast, easy and reliable testing for anything that runs in a browser.',
+ });
+ })
+ // note that the value here is the returned value of the 2nd request
+ // which is the new post object
+ .then((response) => {
+ expect(response).property('status').to.equal(201); // new entity created
+ expect(response).property('body').to.contain({
+ title: 'Cypress Test Runner',
+ });
+
+ // we don't know the exact post id - only that it will be > 100
+ // since JSONPlaceholder has built-in 100 posts
+ expect(response.body)
+ .property('id')
+ .to.be.a('number')
+ .and.to.be.gt(100);
+
+ // we don't know the user id here - since it was in above closure
+ // so in this test just confirm that the property is there
+ expect(response.body).property('userId').to.be.a('number');
+ });
+ });
+
+ it('cy.request() - save response in the shared test context', () => {
+ // https://on.cypress.io/variables-and-aliases
+ cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
+ .its('body')
+ .its('0') // yields the first element of the returned list
+ .as('user') // saves the object in the test context
+ .then(function () {
+ // NOTE 👀
+ // By the time this callback runs the "as('user')" command
+ // has saved the user object in the test context.
+ // To access the test context we need to use
+ // the "function () { ... }" callback form,
+ // otherwise "this" points at a wrong or undefined object!
+ cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
+ userId: this.user.id,
+ title: 'Cypress Test Runner',
+ body:
+ 'Fast, easy and reliable testing for anything that runs in a browser.',
+ })
+ .its('body')
+ .as('post'); // save the new post from the response
+ })
+ .then(function () {
+ // When this callback runs, both "cy.request" API commands have finished
+ // and the test context has "user" and "post" objects set.
+ // Let's verify them.
+ expect(this.post, 'post has the right user id')
+ .property('userId')
+ .to.equal(this.user.id);
+ });
+ });
+
+ it('cy.intercept() - route responses to matching requests', () => {
+ // https://on.cypress.io/intercept
+
+ let message = 'whoa, this comment does not exist';
+
+ // Listen to GET to comments/1
+ cy.intercept('GET', '**/comments/*').as('getComment');
+
+ // we have code that gets a comment when
+ // the button is clicked in scripts.js
+ cy.get('.network-btn').click();
+
+ // https://on.cypress.io/wait
+ cy.wait('@getComment')
+ .its('response.statusCode')
+ .should('be.oneOf', [200, 304]);
+
+ // Listen to POST to comments
+ cy.intercept('POST', '**/comments').as('postComment');
+
+ // we have code that posts a comment when
+ // the button is clicked in scripts.js
+ cy.get('.network-post').click();
+ cy.wait('@postComment').should(({ request, response }) => {
+ expect(request.body).to.include('email');
+ expect(request.headers).to.have.property('content-type');
+ expect(response && response.body).to.have.property(
+ 'name',
+ 'Using POST in cy.intercept()'
+ );
+ });
+
+ // Stub a response to PUT comments/ ****
+ cy.intercept(
+ {
+ method: 'PUT',
+ url: '**/comments/*',
+ },
+ {
+ statusCode: 404,
+ body: { error: message },
+ headers: { 'access-control-allow-origin': '*' },
+ delayMs: 500,
+ }
+ ).as('putComment');
+
+ // we have code that puts a comment when
+ // the button is clicked in scripts.js
+ cy.get('.network-put').click();
+
+ cy.wait('@putComment');
+
+ // our 404 statusCode logic in scripts.js executed
+ cy.get('.network-put-comment').should('contain', message);
+ });
+});
diff --git a/cypress/integration/examples/querying.spec.js b/cypress/integration/examples/querying.spec.js
new file mode 100644
index 0000000..d385151
--- /dev/null
+++ b/cypress/integration/examples/querying.spec.js
@@ -0,0 +1,106 @@
+///
+
+context('Querying', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/querying');
+ });
+
+ // The most commonly used query is 'cy.get()', you can
+ // think of this like the '$' in jQuery
+
+ it('cy.get() - query DOM elements', () => {
+ // https://on.cypress.io/get
+
+ cy.get('#query-btn').should('contain', 'Button');
+
+ cy.get('.query-btn').should('contain', 'Button');
+
+ cy.get('#querying .well>button:first').should('contain', 'Button');
+ // ↲
+ // Use CSS selectors just like jQuery
+
+ cy.get('[data-test-id="test-example"]').should('have.class', 'example');
+
+ // 'cy.get()' yields jQuery object, you can get its attribute
+ // by invoking `.attr()` method
+ cy.get('[data-test-id="test-example"]')
+ .invoke('attr', 'data-test-id')
+ .should('equal', 'test-example');
+
+ // or you can get element's CSS property
+ cy.get('[data-test-id="test-example"]')
+ .invoke('css', 'position')
+ .should('equal', 'static');
+
+ // or use assertions directly during 'cy.get()'
+ // https://on.cypress.io/assertions
+ cy.get('[data-test-id="test-example"]')
+ .should('have.attr', 'data-test-id', 'test-example')
+ .and('have.css', 'position', 'static');
+ });
+
+ it('cy.contains() - query DOM elements with matching content', () => {
+ // https://on.cypress.io/contains
+ cy.get('.query-list').contains('bananas').should('have.class', 'third');
+
+ // we can pass a regexp to `.contains()`
+ cy.get('.query-list').contains(/^b\w+/).should('have.class', 'third');
+
+ cy.get('.query-list').contains('apples').should('have.class', 'first');
+
+ // passing a selector to contains will
+ // yield the selector containing the text
+ cy.get('#querying')
+ .contains('ul', 'oranges')
+ .should('have.class', 'query-list');
+
+ cy.get('.query-button').contains('Save Form').should('have.class', 'btn');
+ });
+
+ it('.within() - query DOM elements within a specific element', () => {
+ // https://on.cypress.io/within
+ cy.get('.query-form').within(() => {
+ cy.get('input:first').should('have.attr', 'placeholder', 'Email');
+ cy.get('input:last').should('have.attr', 'placeholder', 'Password');
+ });
+ });
+
+ it('cy.root() - query the root DOM element', () => {
+ // https://on.cypress.io/root
+
+ // By default, root is the document
+ cy.root().should('match', 'html');
+
+ cy.get('.query-ul').within(() => {
+ // In this within, the root is now the ul DOM element
+ cy.root().should('have.class', 'query-ul');
+ });
+ });
+
+ it('best practices - selecting elements', () => {
+ // https://on.cypress.io/best-practices#Selecting-Elements
+ cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
+ // Worst - too generic, no context
+ cy.get('button').click();
+
+ // Bad. Coupled to styling. Highly subject to change.
+ cy.get('.btn.btn-large').click();
+
+ // Average. Coupled to the `name` attribute which has HTML semantics.
+ cy.get('[name=submission]').click();
+
+ // Better. But still coupled to styling or JS event listeners.
+ cy.get('#main').click();
+
+ // Slightly better. Uses an ID but also ensures the element
+ // has an ARIA role attribute
+ cy.get('#main[role=button]').click();
+
+ // Much better. But still coupled to text content that may change.
+ cy.contains('Submit').click();
+
+ // Best. Insulated from all changes.
+ cy.get('[data-cy=submit]').click();
+ });
+ });
+});
diff --git a/cypress/integration/examples/spies_stubs_clocks.spec.js b/cypress/integration/examples/spies_stubs_clocks.spec.js
new file mode 100644
index 0000000..511b5a4
--- /dev/null
+++ b/cypress/integration/examples/spies_stubs_clocks.spec.js
@@ -0,0 +1,217 @@
+///
+// remove no check once Cypress.sinon is typed
+// https://github.com/cypress-io/cypress/issues/6720
+
+context('Spies, Stubs, and Clock', () => {
+ it('cy.spy() - wrap a method in a spy', () => {
+ // https://on.cypress.io/spy
+ cy.visit('https://example.cypress.io/commands/spies-stubs-clocks');
+
+ const obj = {
+ foo() {},
+ };
+
+ const spy = cy.spy(obj, 'foo').as('anyArgs');
+
+ obj.foo();
+
+ expect(spy).to.be.called;
+ });
+
+ it('cy.spy() retries until assertions pass', () => {
+ cy.visit('https://example.cypress.io/commands/spies-stubs-clocks');
+
+ const obj = {
+ /**
+ * Prints the argument passed
+ * @param x {any}
+ */
+ foo(x) {
+ console.log('obj.foo called with', x);
+ },
+ };
+
+ cy.spy(obj, 'foo').as('foo');
+
+ setTimeout(() => {
+ obj.foo('first');
+ }, 500);
+
+ setTimeout(() => {
+ obj.foo('second');
+ }, 2500);
+
+ cy.get('@foo').should('have.been.calledTwice');
+ });
+
+ it('cy.stub() - create a stub and/or replace a function with stub', () => {
+ // https://on.cypress.io/stub
+ cy.visit('https://example.cypress.io/commands/spies-stubs-clocks');
+
+ const obj = {
+ /**
+ * prints both arguments to the console
+ * @param a {string}
+ * @param b {string}
+ */
+ foo(a, b) {
+ console.log('a', a, 'b', b);
+ },
+ };
+
+ const stub = cy.stub(obj, 'foo').as('foo');
+
+ obj.foo('foo', 'bar');
+
+ expect(stub).to.be.called;
+ });
+
+ it('cy.clock() - control time in the browser', () => {
+ // https://on.cypress.io/clock
+
+ // create the date in UTC so its always the same
+ // no matter what local timezone the browser is running in
+ const now = new Date(Date.UTC(2017, 2, 14)).getTime();
+
+ cy.clock(now);
+ cy.visit('https://example.cypress.io/commands/spies-stubs-clocks');
+ cy.get('#clock-div').click().should('have.text', '1489449600');
+ });
+
+ it('cy.tick() - move time in the browser', () => {
+ // https://on.cypress.io/tick
+
+ // create the date in UTC so its always the same
+ // no matter what local timezone the browser is running in
+ const now = new Date(Date.UTC(2017, 2, 14)).getTime();
+
+ cy.clock(now);
+ cy.visit('https://example.cypress.io/commands/spies-stubs-clocks');
+ cy.get('#tick-div').click().should('have.text', '1489449600');
+
+ cy.tick(10000); // 10 seconds passed
+ cy.get('#tick-div').click().should('have.text', '1489449610');
+ });
+
+ it('cy.stub() matches depending on arguments', () => {
+ // see all possible matchers at
+ // https://sinonjs.org/releases/latest/matchers/
+ const greeter = {
+ /**
+ * Greets a person
+ * @param {string} name
+ */
+ greet(name) {
+ return `Hello, ${name}!`;
+ },
+ };
+
+ cy.stub(greeter, 'greet')
+ .callThrough() // if you want non-matched calls to call the real method
+ .withArgs(Cypress.sinon.match.string)
+ .returns('Hi')
+ .withArgs(Cypress.sinon.match.number)
+ .throws(new Error('Invalid name'));
+
+ expect(greeter.greet('World')).to.equal('Hi');
+ // @ts-ignore
+ expect(() => greeter.greet(42)).to.throw('Invalid name');
+ expect(greeter.greet).to.have.been.calledTwice;
+
+ // non-matched calls goes the actual method
+ // @ts-ignore
+ expect(greeter.greet()).to.equal('Hello, undefined!');
+ });
+
+ it('matches call arguments using Sinon matchers', () => {
+ // see all possible matchers at
+ // https://sinonjs.org/releases/latest/matchers/
+ const calculator = {
+ /**
+ * returns the sum of two arguments
+ * @param a {number}
+ * @param b {number}
+ */
+ add(a, b) {
+ return a + b;
+ },
+ };
+
+ const spy = cy.spy(calculator, 'add').as('add');
+
+ expect(calculator.add(2, 3)).to.equal(5);
+
+ // if we want to assert the exact values used during the call
+ expect(spy).to.be.calledWith(2, 3);
+
+ // let's confirm "add" method was called with two numbers
+ expect(spy).to.be.calledWith(
+ Cypress.sinon.match.number,
+ Cypress.sinon.match.number
+ );
+
+ // alternatively, provide the value to match
+ expect(spy).to.be.calledWith(
+ Cypress.sinon.match(2),
+ Cypress.sinon.match(3)
+ );
+
+ // match any value
+ expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3);
+
+ // match any value from a list
+ expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3);
+
+ /**
+ * Returns true if the given number is event
+ * @param {number} x
+ */
+ const isEven = (x) => x % 2 === 0;
+
+ // expect the value to pass a custom predicate function
+ // the second argument to "sinon.match(predicate, message)" is
+ // shown if the predicate does not pass and assertion fails
+ expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3);
+
+ /**
+ * Returns a function that checks if a given number is larger than the limit
+ * @param {number} limit
+ * @returns {(x: number) => boolean}
+ */
+ const isGreaterThan = (limit) => (x) => x > limit;
+
+ /**
+ * Returns a function that checks if a given number is less than the limit
+ * @param {number} limit
+ * @returns {(x: number) => boolean}
+ */
+ const isLessThan = (limit) => (x) => x < limit;
+
+ // you can combine several matchers using "and", "or"
+ expect(spy).to.be.calledWith(
+ Cypress.sinon.match.number,
+ Cypress.sinon
+ .match(isGreaterThan(2), '> 2')
+ .and(Cypress.sinon.match(isLessThan(4), '< 4'))
+ );
+
+ expect(spy).to.be.calledWith(
+ Cypress.sinon.match.number,
+ Cypress.sinon
+ .match(isGreaterThan(200), '> 200')
+ .or(Cypress.sinon.match(3))
+ );
+
+ // matchers can be used from BDD assertions
+ cy.get('@add').should(
+ 'have.been.calledWith',
+ Cypress.sinon.match.number,
+ Cypress.sinon.match(3)
+ );
+
+ // you can alias matchers for shorter test code
+ const { match: M } = Cypress.sinon;
+
+ cy.get('@add').should('have.been.calledWith', M.number, M(3));
+ });
+});
diff --git a/cypress/integration/examples/traversal.spec.js b/cypress/integration/examples/traversal.spec.js
new file mode 100644
index 0000000..9c4b403
--- /dev/null
+++ b/cypress/integration/examples/traversal.spec.js
@@ -0,0 +1,116 @@
+///
+
+context('Traversal', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/traversal');
+ });
+
+ it('.children() - get child DOM elements', () => {
+ // https://on.cypress.io/children
+ cy.get('.traversal-breadcrumb')
+ .children('.active')
+ .should('contain', 'Data');
+ });
+
+ it('.closest() - get closest ancestor DOM element', () => {
+ // https://on.cypress.io/closest
+ cy.get('.traversal-badge').closest('ul').should('have.class', 'list-group');
+ });
+
+ it('.eq() - get a DOM element at a specific index', () => {
+ // https://on.cypress.io/eq
+ cy.get('.traversal-list>li').eq(1).should('contain', 'siamese');
+ });
+
+ it('.filter() - get DOM elements that match the selector', () => {
+ // https://on.cypress.io/filter
+ cy.get('.traversal-nav>li').filter('.active').should('contain', 'About');
+ });
+
+ it('.find() - get descendant DOM elements of the selector', () => {
+ // https://on.cypress.io/find
+ cy.get('.traversal-pagination')
+ .find('li')
+ .find('a')
+ .should('have.length', 7);
+ });
+
+ it('.first() - get first DOM element', () => {
+ // https://on.cypress.io/first
+ cy.get('.traversal-table td').first().should('contain', '1');
+ });
+
+ it('.last() - get last DOM element', () => {
+ // https://on.cypress.io/last
+ cy.get('.traversal-buttons .btn').last().should('contain', 'Submit');
+ });
+
+ it('.next() - get next sibling DOM element', () => {
+ // https://on.cypress.io/next
+ cy.get('.traversal-ul')
+ .contains('apples')
+ .next()
+ .should('contain', 'oranges');
+ });
+
+ it('.nextAll() - get all next sibling DOM elements', () => {
+ // https://on.cypress.io/nextall
+ cy.get('.traversal-next-all')
+ .contains('oranges')
+ .nextAll()
+ .should('have.length', 3);
+ });
+
+ it('.nextUntil() - get next sibling DOM elements until next el', () => {
+ // https://on.cypress.io/nextuntil
+ cy.get('#veggies').nextUntil('#nuts').should('have.length', 3);
+ });
+
+ it('.not() - remove DOM elements from set of DOM elements', () => {
+ // https://on.cypress.io/not
+ cy.get('.traversal-disabled .btn')
+ .not('[disabled]')
+ .should('not.contain', 'Disabled');
+ });
+
+ it('.parent() - get parent DOM element from DOM elements', () => {
+ // https://on.cypress.io/parent
+ cy.get('.traversal-mark').parent().should('contain', 'Morbi leo risus');
+ });
+
+ it('.parents() - get parent DOM elements from DOM elements', () => {
+ // https://on.cypress.io/parents
+ cy.get('.traversal-cite').parents().should('match', 'blockquote');
+ });
+
+ it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
+ // https://on.cypress.io/parentsuntil
+ cy.get('.clothes-nav')
+ .find('.active')
+ .parentsUntil('.clothes-nav')
+ .should('have.length', 2);
+ });
+
+ it('.prev() - get previous sibling DOM element', () => {
+ // https://on.cypress.io/prev
+ cy.get('.birds').find('.active').prev().should('contain', 'Lorikeets');
+ });
+
+ it('.prevAll() - get all previous sibling DOM elements', () => {
+ // https://on.cypress.io/prevall
+ cy.get('.fruits-list').find('.third').prevAll().should('have.length', 2);
+ });
+
+ it('.prevUntil() - get all previous sibling DOM elements until el', () => {
+ // https://on.cypress.io/prevuntil
+ cy.get('.foods-list')
+ .find('#nuts')
+ .prevUntil('#veggies')
+ .should('have.length', 3);
+ });
+
+ it('.siblings() - get all sibling DOM elements', () => {
+ // https://on.cypress.io/siblings
+ cy.get('.traversal-pills .active').siblings().should('have.length', 2);
+ });
+});
diff --git a/cypress/integration/examples/utilities.spec.js b/cypress/integration/examples/utilities.spec.js
new file mode 100644
index 0000000..3cd7d27
--- /dev/null
+++ b/cypress/integration/examples/utilities.spec.js
@@ -0,0 +1,111 @@
+///
+
+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 element and set its src to the dataUrl
+ let img = Cypress.$(' ', { 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
+ */
+ function waitOneSecond() {
+ // return a promise that resolves after 1 second
+ // @ts-ignore TS2351 (new Cypress.Promise)
+ return new Cypress.Promise((resolve, reject) => {
+ setTimeout(() => {
+ // set waited to true
+ waited = true;
+
+ // resolve with 'foo' string
+ resolve('foo');
+ }, 1000);
+ });
+ }
+
+ cy.then(() => {
+ // return a promise to cy.then() that
+ // is awaited until it resolves
+ // @ts-ignore TS7006
+ return waitOneSecond().then((str) => {
+ expect(str).to.eq('foo');
+ expect(waited).to.be.true;
+ });
+ });
+ });
+});
diff --git a/cypress/integration/examples/viewport.spec.js b/cypress/integration/examples/viewport.spec.js
new file mode 100644
index 0000000..21293a3
--- /dev/null
+++ b/cypress/integration/examples/viewport.spec.js
@@ -0,0 +1,59 @@
+///
+
+context('Viewport', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/viewport');
+ });
+
+ it('cy.viewport() - set the viewport size and dimension', () => {
+ // https://on.cypress.io/viewport
+
+ cy.get('#navbar').should('be.visible');
+ cy.viewport(320, 480);
+
+ // the navbar should have collapse since our screen is smaller
+ cy.get('#navbar').should('not.be.visible');
+ cy.get('.navbar-toggle').should('be.visible').click();
+ cy.get('.nav').find('a').should('be.visible');
+
+ // lets see what our app looks like on a super large screen
+ cy.viewport(2999, 2999);
+
+ // cy.viewport() accepts a set of preset sizes
+ // to easily set the screen to a device's width and height
+
+ // We added a cy.wait() between each viewport change so you can see
+ // the change otherwise it is a little too fast to see :)
+
+ cy.viewport('macbook-15');
+ cy.wait(200);
+ cy.viewport('macbook-13');
+ cy.wait(200);
+ cy.viewport('macbook-11');
+ cy.wait(200);
+ cy.viewport('ipad-2');
+ cy.wait(200);
+ cy.viewport('ipad-mini');
+ cy.wait(200);
+ cy.viewport('iphone-6+');
+ cy.wait(200);
+ cy.viewport('iphone-6');
+ cy.wait(200);
+ cy.viewport('iphone-5');
+ cy.wait(200);
+ cy.viewport('iphone-4');
+ cy.wait(200);
+ cy.viewport('iphone-3');
+ cy.wait(200);
+
+ // cy.viewport() accepts an orientation for all presets
+ // the default orientation is 'portrait'
+ cy.viewport('ipad-2', 'portrait');
+ cy.wait(200);
+ cy.viewport('iphone-4', 'landscape');
+ cy.wait(200);
+
+ // The viewport will be reset back to the default dimensions
+ // in between tests (the default can be set in cypress.json)
+ });
+});
diff --git a/cypress/integration/examples/waiting.spec.js b/cypress/integration/examples/waiting.spec.js
new file mode 100644
index 0000000..6f45dc1
--- /dev/null
+++ b/cypress/integration/examples/waiting.spec.js
@@ -0,0 +1,33 @@
+///
+
+context('Waiting', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/waiting');
+ });
+ // BE CAREFUL of adding unnecessary wait times.
+ // https://on.cypress.io/best-practices#Unnecessary-Waiting
+
+ // https://on.cypress.io/wait
+ it('cy.wait() - wait for a specific amount of time', () => {
+ cy.get('.wait-input1').type('Wait 1000ms after typing');
+ cy.wait(1000);
+ cy.get('.wait-input2').type('Wait 1000ms after typing');
+ cy.wait(1000);
+ cy.get('.wait-input3').type('Wait 1000ms after typing');
+ cy.wait(1000);
+ });
+
+ it('cy.wait() - wait for a specific route', () => {
+ // Listen to GET to comments/1
+ cy.intercept('GET', '**/comments/*').as('getComment');
+
+ // we have code that gets a comment when
+ // the button is clicked in scripts.js
+ cy.get('.network-btn').click();
+
+ // wait for GET comments/1
+ cy.wait('@getComment')
+ .its('response.statusCode')
+ .should('be.oneOf', [200, 304]);
+ });
+});
diff --git a/cypress/integration/examples/window.spec.js b/cypress/integration/examples/window.spec.js
new file mode 100644
index 0000000..2deea94
--- /dev/null
+++ b/cypress/integration/examples/window.spec.js
@@ -0,0 +1,22 @@
+///
+
+context('Window', () => {
+ beforeEach(() => {
+ cy.visit('https://example.cypress.io/commands/window');
+ });
+
+ it('cy.window() - get the global window object', () => {
+ // https://on.cypress.io/window
+ cy.window().should('have.property', 'top');
+ });
+
+ it('cy.document() - get the document object', () => {
+ // https://on.cypress.io/document
+ cy.document().should('have.property', 'charset').and('eq', 'UTF-8');
+ });
+
+ it('cy.title() - get the title', () => {
+ // https://on.cypress.io/title
+ cy.title().should('include', 'Kitchen Sink');
+ });
+});
diff --git a/cypress/integration/sample.spec.js b/cypress/integration/sample.spec.js
new file mode 100644
index 0000000..14e6bec
--- /dev/null
+++ b/cypress/integration/sample.spec.js
@@ -0,0 +1,20 @@
+describe('Example', () => {
+ it('cypress working', () => {
+ expect(true).to.equal(true);
+ });
+
+ it('successfully redirects to main', () => {
+ cy.intercept('POST', 'http://localhost:3001/tracks', {
+ fixture: 'tracks.json',
+ }).as('getTracks');
+
+ cy.intercept('POST', 'http://localhost:3001/artists', {
+ fixture: 'artists.json',
+ }).as('getArtists');
+
+ //INSERT YOUR LOCAL URL TO /MAIN AS A STRING, WITH THE CODE AND EVERYTHING!!!
+ cy.visit();
+
+ cy.wait(['@getTracks', '@getArtists']);
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..59b2bab
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,22 @@
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+// eslint-disable-next-line no-unused-vars
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..119ab03
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
diff --git a/cypress/support/index.js b/cypress/support/index.js
new file mode 100644
index 0000000..d68db96
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/package-lock.json b/package-lock.json
index 0c3ceda..86b8d1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -222,6 +222,152 @@
"chalk": "^4.0.0"
}
},
+ "@cypress/listr-verbose-renderer": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
+ "integrity": "sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-cursor": "^1.0.2",
+ "date-fns": "^1.27.2",
+ "figures": "^1.7.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "cli-cursor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+ "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^1.0.1"
+ }
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "onetime": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
+ "dev": true
+ },
+ "restore-cursor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+ "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+ "dev": true,
+ "requires": {
+ "exit-hook": "^1.0.0",
+ "onetime": "^1.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
+ },
+ "@cypress/request": {
+ "version": "2.88.5",
+ "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.5.tgz",
+ "integrity": "sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==",
+ "dev": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true
+ }
+ }
+ },
+ "@cypress/xvfb": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz",
+ "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==",
+ "dev": true,
+ "requires": {
+ "debug": "^3.1.0",
+ "lodash.once": "^4.1.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
@@ -257,6 +403,15 @@
"strip-json-comments": "^3.1.1"
}
},
+ "@samverschueren/stream-to-observable": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz",
+ "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==",
+ "dev": true,
+ "requires": {
+ "any-observable": "^0.3.0"
+ }
+ },
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -278,6 +433,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
+ "@types/node": {
+ "version": "12.12.50",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.50.tgz",
+ "integrity": "sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==",
+ "dev": true
+ },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -285,6 +446,18 @@
"dev": true,
"optional": true
},
+ "@types/sinonjs__fake-timers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz",
+ "integrity": "sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==",
+ "dev": true
+ },
+ "@types/sizzle": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz",
+ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==",
+ "dev": true
+ },
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -405,6 +578,12 @@
"color-convert": "^1.9.0"
}
},
+ "any-observable": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
+ "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
+ "dev": true
+ },
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@@ -421,6 +600,12 @@
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
},
+ "arch": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
+ "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
+ "dev": true
+ },
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
@@ -538,6 +723,12 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
+ "async": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+ "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==",
+ "dev": true
+ },
"async-foreach": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
@@ -550,6 +741,12 @@
"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
+ },
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -676,6 +873,18 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"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",
@@ -814,6 +1023,18 @@
}
}
},
+ "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
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -982,6 +1203,12 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
+ "check-more-types": {
+ "version": "2.24.0",
+ "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",
+ "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=",
+ "dev": true
+ },
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
@@ -1083,6 +1310,70 @@
"restore-cursor": "^2.0.0"
}
},
+ "cli-table3": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz",
+ "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==",
+ "dev": true,
+ "requires": {
+ "colors": "^1.1.2",
+ "object-assign": "^4.1.0",
+ "string-width": "^4.2.0"
+ }
+ },
+ "cli-truncate": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
+ "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
+ "dev": true,
+ "requires": {
+ "slice-ansi": "0.0.4",
+ "string-width": "^1.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "slice-ansi": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+ "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
+ }
+ },
"cli-width": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
@@ -1178,6 +1469,13 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
+ "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",
@@ -1187,6 +1485,12 @@
"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
+ },
"commitizen": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.3.tgz",
@@ -1263,6 +1567,12 @@
}
}
},
+ "common-tags": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
+ "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==",
+ "dev": true
+ },
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -1275,6 +1585,18 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
"configstore": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
@@ -1428,6 +1750,123 @@
"array-find-index": "^1.0.1"
}
},
+ "cypress": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-6.7.1.tgz",
+ "integrity": "sha512-MC9yt1GqpL4WVDQ0STI89K+PdLeC3T3NuAb2N61d6vYGR9pJy8w3Fqe0OWZwaRTJtg9eAyHXPGmFsyKeNQ3tmg==",
+ "dev": true,
+ "requires": {
+ "@cypress/listr-verbose-renderer": "^0.4.1",
+ "@cypress/request": "^2.88.5",
+ "@cypress/xvfb": "^1.2.4",
+ "@types/node": "12.12.50",
+ "@types/sinonjs__fake-timers": "^6.0.1",
+ "@types/sizzle": "^2.3.2",
+ "arch": "^2.1.2",
+ "blob-util": "2.0.2",
+ "bluebird": "^3.7.2",
+ "cachedir": "^2.3.0",
+ "chalk": "^4.1.0",
+ "check-more-types": "^2.24.0",
+ "cli-table3": "~0.6.0",
+ "commander": "^5.1.0",
+ "common-tags": "^1.8.0",
+ "dayjs": "^1.9.3",
+ "debug": "4.3.2",
+ "eventemitter2": "^6.4.2",
+ "execa": "^4.0.2",
+ "executable": "^4.1.1",
+ "extract-zip": "^1.7.0",
+ "fs-extra": "^9.0.1",
+ "getos": "^3.2.1",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.3.2",
+ "lazy-ass": "^1.6.0",
+ "listr": "^0.14.3",
+ "lodash": "^4.17.19",
+ "log-symbols": "^4.0.0",
+ "minimist": "^1.2.5",
+ "moment": "^2.29.1",
+ "ospath": "^1.2.2",
+ "pretty-bytes": "^5.4.1",
+ "ramda": "~0.27.1",
+ "request-progress": "^3.0.0",
+ "supports-color": "^7.2.0",
+ "tmp": "~0.2.1",
+ "untildify": "^4.0.0",
+ "url": "^0.11.0",
+ "yauzl": "^2.10.0"
+ },
+ "dependencies": {
+ "cachedir": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
+ "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true
+ }
+ }
+ },
"cz-conventional-changelog": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz",
@@ -1465,6 +1904,18 @@
"assert-plus": "^1.0.0"
}
},
+ "date-fns": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
+ "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
+ "dev": true
+ },
+ "dayjs": {
+ "version": "1.10.4",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
+ "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==",
+ "dev": true
+ },
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -1641,6 +2092,12 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
+ "elegant-spinner": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
+ "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
+ "dev": true
+ },
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -2071,17 +2528,81 @@
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true
},
- "esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "eventemitter2": {
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz",
+ "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==",
+ "dev": true
+ },
+ "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"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ }
+ }
+ },
+ "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"
+ }
+ },
+ "exit-hook": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
"dev": true
},
- "etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
- },
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -2296,6 +2817,35 @@
}
}
},
+ "extract-zip": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
+ "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
+ "dev": true,
+ "requires": {
+ "concat-stream": "^1.6.2",
+ "debug": "^2.6.9",
+ "mkdirp": "^0.5.4",
+ "yauzl": "^2.10.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
+ }
+ }
+ },
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -2320,6 +2870,15 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
+ "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": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@@ -2635,6 +3194,15 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true
},
+ "getos": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz",
+ "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==",
+ "dev": true,
+ "requires": {
+ "async": "^3.2.0"
+ }
+ },
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -2911,6 +3479,12 @@
"sshpk": "^1.7.0"
}
},
+ "human-signals": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+ "dev": true
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3313,6 +3887,15 @@
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"dev": true
},
+ "is-observable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
+ "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
+ "dev": true,
+ "requires": {
+ "symbol-observable": "^1.1.0"
+ }
+ },
"is-path-inside": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
@@ -3328,6 +3911,12 @@
"isobject": "^3.0.1"
}
},
+ "is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "dev": true
+ },
"is-regex": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz",
@@ -3338,6 +3927,12 @@
"has-symbols": "^1.0.1"
}
},
+ "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-string": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
@@ -3359,6 +3954,12 @@
"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-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -3545,6 +4146,12 @@
"package-json": "^6.3.0"
}
},
+ "lazy-ass": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
+ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=",
+ "dev": true
+ },
"levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3562,6 +4169,145 @@
"dev": true,
"optional": true
},
+ "listr": {
+ "version": "0.14.3",
+ "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz",
+ "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==",
+ "dev": true,
+ "requires": {
+ "@samverschueren/stream-to-observable": "^0.3.0",
+ "is-observable": "^1.1.0",
+ "is-promise": "^2.1.0",
+ "is-stream": "^1.1.0",
+ "listr-silent-renderer": "^1.1.1",
+ "listr-update-renderer": "^0.5.0",
+ "listr-verbose-renderer": "^0.5.0",
+ "p-map": "^2.0.0",
+ "rxjs": "^6.3.3"
+ },
+ "dependencies": {
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true
+ }
+ }
+ },
+ "listr-silent-renderer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
+ "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
+ "dev": true
+ },
+ "listr-update-renderer": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz",
+ "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.1.3",
+ "cli-truncate": "^0.2.1",
+ "elegant-spinner": "^1.0.1",
+ "figures": "^1.7.0",
+ "indent-string": "^3.0.0",
+ "log-symbols": "^1.0.2",
+ "log-update": "^2.3.0",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+ "dev": true
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "indent-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+ "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=",
+ "dev": true
+ },
+ "log-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
+ "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
+ "dev": true,
+ "requires": {
+ "chalk": "^1.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+ "dev": true
+ }
+ }
+ },
+ "listr-verbose-renderer": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz",
+ "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.4.1",
+ "cli-cursor": "^2.1.0",
+ "date-fns": "^1.27.2",
+ "figures": "^2.0.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ }
+ }
+ },
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@@ -3595,6 +4341,76 @@
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=",
"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"
+ }
+ },
+ "log-update": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz",
+ "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^3.0.0",
+ "cli-cursor": "^2.0.0",
+ "wrap-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "wrap-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz",
+ "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=",
+ "dev": true,
+ "requires": {
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0"
+ }
+ }
+ }
+ },
"longest": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz",
@@ -3794,6 +4610,12 @@
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -4169,6 +4991,15 @@
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==",
"dev": true
},
+ "npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@@ -4358,6 +5189,12 @@
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
},
+ "ospath": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
+ "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=",
+ "dev": true
+ },
"p-cancelable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
@@ -4382,6 +5219,12 @@
"p-limit": "^1.1.0"
}
},
+ "p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true
+ },
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@@ -4481,6 +5324,12 @@
"pify": "^2.0.0"
}
},
+ "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",
@@ -4551,6 +5400,12 @@
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true
},
+ "pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "dev": true
+ },
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -4624,6 +5479,18 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
+ "querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+ "dev": true
+ },
+ "ramda": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
+ "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==",
+ "dev": true
+ },
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -4854,6 +5721,15 @@
}
}
},
+ "request-progress": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
+ "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=",
+ "dev": true,
+ "requires": {
+ "throttleit": "^1.0.0"
+ }
+ },
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -5521,6 +6397,12 @@
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
"dev": true
},
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true
+ },
"strip-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
@@ -5574,6 +6456,12 @@
"has-flag": "^3.0.0"
}
},
+ "symbol-observable": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
+ "dev": true
+ },
"table": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz",
@@ -5640,6 +6528,12 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
+ "throttleit": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
+ "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=",
+ "dev": true
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@@ -5804,6 +6698,12 @@
"mime-types": "~2.1.24"
}
},
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+ "dev": true
+ },
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -5923,6 +6823,12 @@
}
}
},
+ "untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true
+ },
"update-notifier": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
@@ -6010,6 +6916,24 @@
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
"dev": true
},
+ "url": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+ "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+ "dev": true,
+ "requires": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+ "dev": true
+ }
+ }
+ },
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -6400,6 +7324,16 @@
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
+ },
+ "yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
+ "dev": true,
+ "requires": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
}
}
}
diff --git a/package.json b/package.json
index fcfdfd4..79b46b3 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "listspotter",
"version": "1.0.0",
"description": "A Spotify API based app that allows you to create a playlist from all your saved songs in Spotify, based on their genres.",
- "main": "index.js",
+ "main": "index",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
@@ -18,6 +18,7 @@
"homepage": "https://github.com/ascolm/listspotter#readme",
"devDependencies": {
"commitizen": "^4.2.3",
+ "cypress": "^6.7.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.21.0",
"eslint-config-standard": "^16.0.2",
diff --git a/server/.prettierrc.ts b/server/.prettierrc.ts
new file mode 100644
index 0000000..b0a881e
--- /dev/null
+++ b/server/.prettierrc.ts
@@ -0,0 +1,5 @@
+export default {
+ bracketSpacing: true,
+ singleQuote: true,
+ trailingComma: 'all',
+};
diff --git a/server/config.ts b/server/config.ts
new file mode 100644
index 0000000..ca5dc2c
--- /dev/null
+++ b/server/config.ts
@@ -0,0 +1,4 @@
+export default {
+ clientID: 'd49ee10b034843b1aabd579ac1901e42',
+ clientSecret: '04414237048a48518f44be66ede1c734',
+};
diff --git a/server/controller/controller-helpers.js b/server/controller/controller-helpers.js
index a827d6b..709bcb3 100644
--- a/server/controller/controller-helpers.js
+++ b/server/controller/controller-helpers.js
@@ -1,15 +1,50 @@
-exports.requestWhileQueued = async (initialQueue, limit, handler) => {
-
- let queueArr = initialQueue.slice();
-
- while (queueArr.length > 0) {
- if (queueArr.length < limit) {
- await handler(queueArr);
- queueArr = [];
- } else {
- let tracksToQuery = queueArr.splice(0, limit);
- await handler(tracksToQuery);
+'use strict';
+var __awaiter =
+ (this && this.__awaiter) ||
+ function (thisArg, _arguments, P, generator) {
+ function adopt(value) {
+ return value instanceof P
+ ? value
+ : new P(function (resolve) {
+ resolve(value);
+ });
}
- }
-};
-
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) {
+ try {
+ step(generator.next(value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function rejected(value) {
+ try {
+ step(generator['throw'](value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function step(result) {
+ result.done
+ ? resolve(result.value)
+ : adopt(result.value).then(fulfilled, rejected);
+ }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+ };
+Object.defineProperty(exports, '__esModule', { value: true });
+exports.requestWhileQueued = void 0;
+const requestWhileQueued = (initialQueue, limit, handler) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ let queueArr = initialQueue.slice();
+ while (queueArr.length > 0) {
+ if (queueArr.length < limit) {
+ yield handler(queueArr);
+ queueArr = [];
+ } else {
+ let tracksToQuery = queueArr.splice(0, limit);
+ yield handler(tracksToQuery);
+ }
+ }
+ });
+exports.requestWhileQueued = requestWhileQueued;
diff --git a/server/controller/controller-helpers.ts b/server/controller/controller-helpers.ts
new file mode 100644
index 0000000..d83f570
--- /dev/null
+++ b/server/controller/controller-helpers.ts
@@ -0,0 +1,15 @@
+export const requestWhileQueued = async (initialQueue: any[], limit: number, handler: Function) => {
+
+ let queueArr = initialQueue.slice();
+
+ while (queueArr.length > 0) {
+ if (queueArr.length < limit) {
+ await handler(queueArr);
+ queueArr = [];
+ } else {
+ let tracksToQuery = queueArr.splice(0, limit);
+ await handler(tracksToQuery);
+ }
+ }
+};
+
diff --git a/server/controller/controller.js b/server/controller/controller.js
index b097557..e4d897b 100644
--- a/server/controller/controller.js
+++ b/server/controller/controller.js
@@ -1,8 +1,46 @@
-const axios = require('axios');
-const { renderSync } = require('node-sass');
-const modeller = require('../modeller/modeller');
-const { requestWhileQueued } = require('./controller-helpers');
-
+'use strict';
+var __awaiter =
+ (this && this.__awaiter) ||
+ function (thisArg, _arguments, P, generator) {
+ function adopt(value) {
+ return value instanceof P
+ ? value
+ : new P(function (resolve) {
+ resolve(value);
+ });
+ }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) {
+ try {
+ step(generator.next(value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function rejected(value) {
+ try {
+ step(generator['throw'](value));
+ } catch (e) {
+ reject(e);
+ }
+ }
+ function step(result) {
+ result.done
+ ? resolve(result.value)
+ : adopt(result.value).then(fulfilled, rejected);
+ }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+ };
+var __importDefault =
+ (this && this.__importDefault) ||
+ function (mod) {
+ return mod && mod.__esModule ? mod : { default: mod };
+ };
+Object.defineProperty(exports, '__esModule', { value: true });
+exports.createPlaylist = exports.getPlaylistCover = exports.getArtists = exports.getTracks = exports.getTokens = void 0;
+const modeller_1 = __importDefault(require('../modeller/modeller'));
+const controller_helpers_1 = require('./controller-helpers');
const baseUrl = 'https://api.spotify.com/v1';
const spotifyTracksUrl = baseUrl + '/me/tracks';
const spotifyArtistsUrl = baseUrl + '/me/following';
@@ -12,122 +50,134 @@ const spotifyPlaylistUrl = baseUrl + '/playlists';
const saveTrackRequestLimit = 100; // max number of tracks allowed in a single "save tracks to playlist" request in spotify
const offsetIncrement = 50; // max number of tracks allowed in a single "get saved tracks" request in spotify
const msBetweenTrackRequests = 250;
-
// TODO: TOKEN TO BE PER USER SESSION RATHER THAN ONE FOR THE WHOLE SERVER: GET USER ID AFTER RECEIVING AUTH TOKEN, SAVE AS ID TOKEN PAIR, SEND BACK USER ID TO CLIENT FOR USE IN SUBSEQUENT REQUESTS
-let tokens ='';
-
+let tokens = '';
// TODO: CHECK ERROR HANDLING IN CATCH METHODS // CREATE CUSTOM HANDLER MIDDLEWARE
// TODO: REFACTOR AXIOS REQUESTS INTO MODELLER
// TODO: separate gettokens/checktokens part as a middleware to be passed through in each request
-
-exports.getTokens = async (req, res, next) => {
- const { code } = req.body;
- tokens = await modeller.requestToken(code, next);
- res.sendStatus(200);
-};
-
-exports.getTracks = async (req, res, next) => {
- const {code} = req.body;
- let initialOffset = 0;
- let trackData = [];
-
- if (!tokens) {
- tokens = await modeller.requestToken(code, next);
- }
-
- function timeOutPromise () {
- return new Promise((resolve) => setTimeout(() => resolve(), msBetweenTrackRequests));
- };
-
- async function fetchTracksAsync (offset) {
- let trackBufferResponse;
-
- try{
- trackBufferResponse = await modeller.requestTracks(spotifyTracksUrl, tokens, offset);
+const getTokens = (req, res, next) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ const { code } = req.body;
+ tokens = yield modeller_1.default.requestToken(code, next);
+ res.sendStatus(200);
+ });
+exports.getTokens = getTokens;
+const getTracks = (req, res, next) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ const { code } = req.body;
+ let initialOffset = 0;
+ let trackData = [];
+ if (!tokens) {
+ tokens = yield modeller_1.default.requestToken(code, next);
+ }
+ function timeOutPromise() {
+ return new Promise((resolve) =>
+ setTimeout(() => resolve(), msBetweenTrackRequests)
+ );
+ }
+ function fetchTracksAsync(offset) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let trackBufferResponse;
+ try {
+ trackBufferResponse = yield modeller_1.default.requestTracks(
+ spotifyTracksUrl,
+ tokens,
+ offset
+ );
+ } catch (err) {
+ console.log('inner error 😎');
+ console.log(err.response.status); // 404
+ if (err.response.status === 404) {
+ yield timeOutPromise();
+ yield fetchTracksAsync(offset);
+ return;
+ }
+ }
+ trackData = [...trackData, ...trackBufferResponse.data.items];
+ console.log('received tracks for offset:', offset);
+ if (trackBufferResponse.data.next) {
+ offset += offsetIncrement;
+ yield fetchTracksAsync(offset);
+ }
+ });
+ }
+ try {
+ yield fetchTracksAsync(initialOffset);
+ console.log('sending ' + trackData.length + ' tracks');
+ res.status(200);
+ res.send(trackData);
} catch (err) {
- console.log('inner error 😎')
- console.log(err.response.status); // 404
- if(err.response.status === 404){
- await timeOutPromise();
- await fetchTracksAsync (offset);
- return;
- }
+ console.log('outer error 😎');
+ console.log('something went wrong while fetching tracks');
+ console.log(err);
+ res.sendStatus(500);
}
-
- trackData = [...trackData, ...trackBufferResponse.data.items];
-
- console.log('received tracks for offset:', offset);
- if (trackBufferResponse.data.next) {
- offset += offsetIncrement;
- await fetchTracksAsync (offset)
+ });
+exports.getTracks = getTracks;
+const getArtists = (req, res, next) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ const { code, offset, nextUrl } = req.body;
+ if (!tokens) {
+ tokens = yield modeller_1.default.requestToken(code, next);
}
- };
-
- try {
- await fetchTracksAsync(initialOffset);
- console.log('sending ' + trackData.length + ' tracks')
- res.status = 200;
- res.send(trackData);
- } catch (err) {
- console.log('outer error 😎')
- console.log('something went wrong while fetching tracks');
- console.log(err);
- res.sendStatus(500);
- }
-};
-
-exports.getArtists = async (req, res, next) => {
- const {code, offset, nextUrl} = req.body;
-
- if (!tokens) {
- tokens = await modeller.requestToken(code, next);
- }
-
- modeller.requestArtists(spotifyArtistsUrl, nextUrl, tokens)
- .then((artistResponse) => {
- res.statusCode = 200;
- res.send(artistResponse.data);
- })
- .catch((err) => console.log(err.response.data));
-};
-
-exports.getPlaylistCover = async (req, res, next) => {
- const {code, playlistId} = req.body;
-
- if (!tokens) {
- tokens = await modeller.requestToken(code, next);
- }
-
- modeller.requestPlaylistCover(spotifyPlaylistUrl, playlistId, tokens)
- .then((coverResponse) => {
- res.statusCode = 200;
- console.log(coverResponse);
- res.send(coverResponse.data);
- })
- .catch((err) => console.log(err.response.data));
-};
-
-
-exports.createPlaylist = async (req, res, next) => {
- const {code, playlistName, trackURIs} = req.body;
-
- if (!tokens) {
- tokens = await modeller.requestToken(code, next);
- }
-
- const userResponse = await modeller.requestUser(spotifyUserUrl, tokens);
- const userID = userResponse.data.id;
-
- const createPlaylistResponse = await modeller.requestCreatePlaylist(spotifyCreatePlaylistUrl, playlistName, userID, tokens);
- const playlistData = createPlaylistResponse.data;
-
- function addTracks (trackArr) {
- return modeller.requestAddTracks(spotifyPlaylistUrl, playlistData.id, trackArr, tokens);
- }
-
- // Spotify has a track limit per request, so below we check if multiple requests are necessary (TrackQueue is all the tracks to be added in a queue form, with multiple tracks "shifting" per request.)
- requestWhileQueued(trackURIs, saveTrackRequestLimit, addTracks);
-
- res.status(200);
- res.send(JSON.stringify(playlistData));
-};
\ No newline at end of file
+ modeller_1.default
+ .requestArtists(spotifyArtistsUrl, nextUrl, tokens)
+ .then((artistResponse) => {
+ res.statusCode = 200;
+ res.send(artistResponse.data);
+ })
+ .catch((err) => console.log(err.response.data));
+ });
+exports.getArtists = getArtists;
+const getPlaylistCover = (req, res, next) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ const { code, playlistId } = req.body;
+ if (!tokens) {
+ tokens = yield modeller_1.default.requestToken(code, next);
+ }
+ modeller_1.default
+ .requestPlaylistCover(spotifyPlaylistUrl, playlistId, tokens)
+ .then((coverResponse) => {
+ res.statusCode = 200;
+ console.log(coverResponse);
+ res.send(coverResponse.data);
+ })
+ .catch((err) => console.log(err.response.data));
+ });
+exports.getPlaylistCover = getPlaylistCover;
+const createPlaylist = (req, res, next) =>
+ __awaiter(void 0, void 0, void 0, function* () {
+ const { code, playlistName, trackURIs } = req.body;
+ if (!tokens) {
+ tokens = yield modeller_1.default.requestToken(code, next);
+ }
+ const userResponse = yield modeller_1.default.requestUser(
+ spotifyUserUrl,
+ tokens
+ );
+ const userID = userResponse.data.id;
+ const createPlaylistResponse = yield modeller_1.default.requestCreatePlaylist(
+ spotifyCreatePlaylistUrl,
+ playlistName,
+ userID,
+ tokens
+ );
+ const playlistData = createPlaylistResponse.data;
+ function addTracks(trackArr) {
+ return modeller_1.default.requestAddTracks(
+ spotifyPlaylistUrl,
+ playlistData.id,
+ trackArr,
+ tokens
+ );
+ }
+ // Spotify has a track limit per request, so below we check if multiple requests are necessary (TrackQueue is all the tracks to be added in a queue form, with multiple tracks "shifting" per request.)
+ controller_helpers_1.requestWhileQueued(
+ trackURIs,
+ saveTrackRequestLimit,
+ addTracks
+ );
+ res.status(200);
+ res.send(JSON.stringify(playlistData));
+ });
+exports.createPlaylist = createPlaylist;
diff --git a/server/controller/controller.ts b/server/controller/controller.ts
new file mode 100644
index 0000000..809117b
--- /dev/null
+++ b/server/controller/controller.ts
@@ -0,0 +1,152 @@
+import { Request, Response, NextFunction } from 'express';
+import axios from "axios";
+import { renderSync } from "node-sass";
+import modeller from "../modeller/modeller";
+import { requestWhileQueued } from "./controller-helpers";
+
+
+const baseUrl = "https://api.spotify.com/v1";
+const spotifyTracksUrl = baseUrl + "/me/tracks";
+const spotifyArtistsUrl = baseUrl + "/me/following";
+const spotifyUserUrl = baseUrl + "/me";
+const spotifyCreatePlaylistUrl = baseUrl + "/users";
+const spotifyPlaylistUrl = baseUrl + "/playlists";
+const saveTrackRequestLimit = 100; // max number of tracks allowed in a single "save tracks to playlist" request in spotify
+const offsetIncrement = 50; // max number of tracks allowed in a single "get saved tracks" request in spotify
+const msBetweenTrackRequests = 250;
+
+// TODO: TOKEN TO BE PER USER SESSION RATHER THAN ONE FOR THE WHOLE SERVER: GET USER ID AFTER RECEIVING AUTH TOKEN, SAVE AS ID TOKEN PAIR, SEND BACK USER ID TO CLIENT FOR USE IN SUBSEQUENT REQUESTS
+let tokens = "";
+
+// TODO: CHECK ERROR HANDLING IN CATCH METHODS // CREATE CUSTOM HANDLER MIDDLEWARE
+// TODO: REFACTOR AXIOS REQUESTS INTO MODELLER
+// TODO: separate gettokens/checktokens part as a middleware to be passed through in each request
+
+export const getTokens = async (req: Request, res: Response, next: NextFunction ) => {
+ const { code } = req.body;
+ tokens = await modeller.requestToken(code, next);
+ res.sendStatus(200);
+};
+
+export const getTracks = async (req: Request, res: Response, next: NextFunction) => {
+ const { code } = req.body;
+ let initialOffset = 0;
+ let trackData: any[] = [];
+
+ if (!tokens) {
+ tokens = await modeller.requestToken(code, next);
+ }
+
+ function timeOutPromise() {
+ return new Promise((resolve) =>
+ setTimeout(() => resolve(), msBetweenTrackRequests)
+ );
+ }
+
+ async function fetchTracksAsync(offset: number) {
+ let trackBufferResponse: any;
+
+ try {
+ trackBufferResponse = await modeller.requestTracks(
+ spotifyTracksUrl,
+ tokens,
+ offset
+ );
+ } catch (err) {
+ console.log("inner error 😎");
+ console.log(err.response.status); // 404
+ if (err.response.status === 404) {
+ await timeOutPromise();
+ await fetchTracksAsync(offset);
+ return;
+ }
+ }
+
+ trackData = [...trackData, ...trackBufferResponse.data.items];
+
+ console.log("received tracks for offset:", offset);
+ if (trackBufferResponse.data.next) {
+ offset += offsetIncrement;
+ await fetchTracksAsync(offset);
+ }
+ }
+
+ try {
+ await fetchTracksAsync(initialOffset);
+ console.log("sending " + trackData.length + " tracks");
+ res.status(200);
+ res.send(trackData);
+ } catch (err) {
+ console.log("outer error 😎");
+ console.log("something went wrong while fetching tracks");
+ console.log(err);
+ res.sendStatus(500);
+ }
+};
+
+export const getArtists = async (req: Request, res: Response, next: NextFunction) => {
+ const { code, offset, nextUrl } = req.body;
+
+ if (!tokens) {
+ tokens = await modeller.requestToken(code, next);
+ }
+
+ modeller
+ .requestArtists(spotifyArtistsUrl, nextUrl, tokens)
+ .then((artistResponse) => {
+ res.statusCode = 200;
+ res.send(artistResponse.data);
+ })
+ .catch((err) => console.log(err.response.data));
+};
+
+export const getPlaylistCover = async (req: Request, res: Response, next: NextFunction) => {
+ const { code, playlistId } = req.body;
+
+ if (!tokens) {
+ tokens = await modeller.requestToken(code, next);
+ }
+
+ modeller
+ .requestPlaylistCover(spotifyPlaylistUrl, playlistId, tokens)
+ .then((coverResponse) => {
+ res.statusCode = 200;
+ console.log(coverResponse);
+ res.send(coverResponse.data);
+ })
+ .catch((err) => console.log(err.response.data));
+};
+
+export const createPlaylist = async (req: Request, res: Response, next: NextFunction) => {
+ const { code, playlistName, trackURIs } = req.body;
+
+ if (!tokens) {
+ tokens = await modeller.requestToken(code, next);
+ }
+
+ const userResponse = await modeller.requestUser(spotifyUserUrl, tokens);
+ const userID = userResponse.data.id;
+
+ const createPlaylistResponse = await modeller.requestCreatePlaylist(
+ spotifyCreatePlaylistUrl,
+ playlistName,
+ userID,
+ tokens
+ );
+ const playlistData = createPlaylistResponse.data;
+
+ function addTracks(trackArr: []) {
+ return modeller.requestAddTracks(
+ spotifyPlaylistUrl,
+ playlistData.id,
+ trackArr,
+ tokens
+ );
+ }
+
+ // Spotify has a track limit per request, so below we check if multiple requests are necessary (TrackQueue is all the tracks to be added in a queue form, with multiple tracks "shifting" per request.)
+ requestWhileQueued(trackURIs, saveTrackRequestLimit, addTracks);
+
+ res.status(200);
+ res.send(JSON.stringify(playlistData));
+};
diff --git a/server/index.js b/server/index.js
index f2a4d9d..0d0866c 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,21 +1,23 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
// DEPENDENCIES
-const express = require('express');
-const morgan = require('morgan');
-const cors = require('cors');
-const router = require('./router');
-
+const express_1 = __importDefault(require("express"));
+const morgan_1 = __importDefault(require("morgan"));
+const cors_1 = __importDefault(require("cors"));
+const router_1 = __importDefault(require("./router"));
// SERVER CONFIG
-const app = express();
+const app = express_1.default();
const port = 3001;
-const host = '127.0.0.1';
-
+const host = "127.0.0.1";
// MIDDLEWARES
-app.use(cors());
-app.use(express.json());
-app.use(morgan('dev'));
-app.use(router);
-
+app.use(cors_1.default());
+app.use(express_1.default.json());
+app.use(morgan_1.default("dev"));
+app.use(router_1.default);
// CONNECTION TO DB & LISTENING TO PORT
app.listen(port, host, () => {
- console.log(`Server running at ${host}:${port}! 🐯`);
-});
\ No newline at end of file
+ console.log(`Server running at ${host}:${port}! 🐯`);
+});
diff --git a/server/index.ts b/server/index.ts
new file mode 100644
index 0000000..a665970
--- /dev/null
+++ b/server/index.ts
@@ -0,0 +1,21 @@
+// DEPENDENCIES
+import express, {Application} from "express";
+import morgan from "morgan";
+import cors from "cors";
+import router from "./router";
+
+// SERVER CONFIG
+const app : Application = express();
+const port = 3001;
+const host = "127.0.0.1";
+
+// MIDDLEWARES
+app.use(cors());
+app.use(express.json());
+app.use(morgan("dev"));
+app.use(router);
+
+// CONNECTION TO DB & LISTENING TO PORT
+app.listen(port, host, () => {
+ console.log(`Server running at ${host}:${port}! 🐯`);
+});
diff --git a/server/modeller/modeller.js b/server/modeller/modeller.js
index cb455f0..ad37d67 100644
--- a/server/modeller/modeller.js
+++ b/server/modeller/modeller.js
@@ -1,93 +1,112 @@
-const axios = require('axios');
-const { clientID, clientSecret } = require('../config');
+'use strict';
+var __importDefault =
+ (this && this.__importDefault) ||
+ function (mod) {
+ return mod && mod.__esModule ? mod : { default: mod };
+ };
+Object.defineProperty(exports, '__esModule', { value: true });
+const axios_1 = __importDefault(require('axios'));
+const config_1 = __importDefault(require('../config'));
const spotifyTokenUrl = 'https://accounts.spotify.com/api/token';
const redirectUri = 'http://localhost:3000/main';
-
-
// TODO: REFACTOR WITH A REQUEST TEMPLATE
-exports.requestToken = (code, next) => {
- return axios.request({
- method: 'POST',
- url: spotifyTokenUrl,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- data: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&client_id=${clientID}&client_secret=${clientSecret}`
- })
+const requestToken = (code, next) => {
+ return axios_1.default
+ .request({
+ method: 'POST',
+ url: spotifyTokenUrl,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ data: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&client_id=${config_1.default.clientID}&client_secret=${config_1.default.clientSecret}`,
+ })
.then((tokenResponse) => {
return tokenResponse.data;
})
.catch(next);
};
-
-exports.requestTracks = (spotifyTracksUrl, tokens, offset) => {
- return axios.request({
+const requestTracks = (spotifyTracksUrl, tokens, offset) => {
+ return axios_1.default.request({
method: 'GET',
url: spotifyTracksUrl + `?offset=${offset}&limit=50`,
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
- 'Content-Type': 'application/x-www-form-urlencoded'
+ Authorization: `Bearer ${tokens['access_token']}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
},
});
};
-
-exports.requestArtists = (spotifyArtistsUrl, nextUrl, tokens) => {
- return axios.request({
+const requestArtists = (spotifyArtistsUrl, nextUrl, tokens) => {
+ return axios_1.default.request({
method: 'GET',
url: nextUrl || spotifyArtistsUrl + '?type=artist&limit=50',
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
+ Authorization: `Bearer ${tokens['access_token']}`,
'Content-Type': 'application/json',
- 'Accept': 'application/json'
+ Accept: 'application/json',
},
});
};
-
-exports.requestUser = (spotifyUserUrl, tokens) => {
- return axios.request({
+const requestUser = (spotifyUserUrl, tokens) => {
+ return axios_1.default.request({
method: 'GET',
url: spotifyUserUrl,
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
- 'Content-Type': 'application/x-www-form-urlencoded'
+ Authorization: `Bearer ${tokens['access_token']}`,
+ 'Content-Type': 'application/x-www-form-urlencoded',
},
});
};
-
-exports.requestPlaylistCover = (spotifyPlaylistUrl, playlistId, tokens) => {
- return axios.request({
+const requestPlaylistCover = (spotifyPlaylistUrl, playlistId, tokens) => {
+ return axios_1.default.request({
method: 'GET',
url: spotifyPlaylistUrl + `/${playlistId}/images`,
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
+ Authorization: `Bearer ${tokens['access_token']}`,
'Content-Type': 'application/json',
- Accept: 'application/json'
+ Accept: 'application/json',
},
});
};
-
-exports.requestCreatePlaylist = (spotifyCreatePlaylistUrl, playlistName, userID, tokens) => {
- return axios.request({
+const requestCreatePlaylist = (
+ spotifyCreatePlaylistUrl,
+ playlistName,
+ userID,
+ tokens
+) => {
+ return axios_1.default.request({
method: 'POST',
url: spotifyCreatePlaylistUrl + `/${userID}/playlists`,
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
- 'Content-Type': 'application/json'
+ Authorization: `Bearer ${tokens['access_token']}`,
+ 'Content-Type': 'application/json',
},
- data: {name: playlistName}
+ data: { name: playlistName },
});
};
-
-exports.requestAddTracks = (spotifySaveTracksUrl, playlistID, trackArr, tokens) => {
- return axios.request({
+const requestAddTracks = (
+ spotifySaveTracksUrl,
+ playlistID,
+ trackArr,
+ tokens
+) => {
+ return axios_1.default.request({
method: 'POST',
url: spotifySaveTracksUrl + `/${playlistID}/tracks`,
headers: {
- 'Authorization': `Bearer ${tokens['access_token']}`,
- 'Content-Type': 'application/json'
+ Authorization: `Bearer ${tokens['access_token']}`,
+ 'Content-Type': 'application/json',
},
data: {
- uris: trackArr
- }
+ uris: trackArr,
+ },
});
-};
\ No newline at end of file
+};
+exports.default = {
+ requestToken,
+ requestTracks,
+ requestArtists,
+ requestUser,
+ requestPlaylistCover,
+ requestCreatePlaylist,
+ requestAddTracks,
+};
diff --git a/server/modeller/modeller.ts b/server/modeller/modeller.ts
new file mode 100644
index 0000000..21717b8
--- /dev/null
+++ b/server/modeller/modeller.ts
@@ -0,0 +1,127 @@
+import axios from "axios";
+import config from "../config";
+
+const spotifyTokenUrl = "https://accounts.spotify.com/api/token";
+const redirectUri = "http://localhost:3000/main";
+import { NextFunction } from "express";
+
+// TODO: REFACTOR WITH A REQUEST TEMPLATE
+const requestToken = (code: string, next: NextFunction) => {
+ return axios
+ .request({
+ method: "POST",
+ url: spotifyTokenUrl,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: `grant_type=authorization_code&code=${code}&redirect_uri=${redirectUri}&client_id=${config.clientID}&client_secret=${config.clientSecret}`,
+ })
+ .then((tokenResponse) => {
+ return tokenResponse.data;
+ })
+ .catch(next);
+};
+
+const requestTracks = (
+ spotifyTracksUrl: string,
+ tokens: any,
+ offset: number
+) => {
+ return axios.request({
+ method: "GET",
+ url: spotifyTracksUrl + `?offset=${offset}&limit=50`,
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+};
+
+const requestArtists = (
+ spotifyArtistsUrl: string,
+ nextUrl: string,
+ tokens: any
+) => {
+ return axios.request({
+ method: "GET",
+ url: nextUrl || spotifyArtistsUrl + "?type=artist&limit=50",
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ });
+};
+
+const requestUser = (spotifyUserUrl: string, tokens: any) => {
+ return axios.request({
+ method: "GET",
+ url: spotifyUserUrl,
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+};
+
+const requestPlaylistCover = (
+ spotifyPlaylistUrl: string,
+ playlistId: string,
+ tokens: any
+) => {
+ return axios.request({
+ method: "GET",
+ url: spotifyPlaylistUrl + `/${playlistId}/images`,
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ });
+};
+
+const requestCreatePlaylist = (
+ spotifyCreatePlaylistUrl: string,
+ playlistName: string,
+ userID: string,
+ tokens: any
+) => {
+ return axios.request({
+ method: "POST",
+ url: spotifyCreatePlaylistUrl + `/${userID}/playlists`,
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/json",
+ },
+ data: { name: playlistName },
+ });
+};
+
+const requestAddTracks = (
+ spotifySaveTracksUrl: string,
+ playlistID: string,
+ trackArr: string[],
+ tokens: any
+) => {
+ return axios.request({
+ method: "POST",
+ url: spotifySaveTracksUrl + `/${playlistID}/tracks`,
+ headers: {
+ Authorization: `Bearer ${tokens["access_token"]}`,
+ "Content-Type": "application/json",
+ },
+ data: {
+ uris: trackArr,
+ },
+ });
+};
+
+export default {
+ requestToken,
+ requestTracks,
+ requestArtists,
+ requestUser,
+ requestPlaylistCover,
+ requestCreatePlaylist,
+ requestAddTracks,
+};
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000..80181f5
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,591 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.34",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
+ "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/cors": {
+ "version": "2.8.10",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
+ "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==",
+ "dev": true
+ },
+ "@types/express": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.11.tgz",
+ "integrity": "sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==",
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.18",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.17.18",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz",
+ "integrity": "sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==",
+ "requires": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
+ },
+ "@types/morgan": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.2.tgz",
+ "integrity": "sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/node": {
+ "version": "14.14.34",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.34.tgz",
+ "integrity": "sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA=="
+ },
+ "@types/node-sass": {
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.1.tgz",
+ "integrity": "sha512-wPOmOEEtbwQiPTIgzUuRSQZ3H5YHinsxRGeZzPSDefAm4ylXWnZG9C0adses8ymyplKK0gwv3JkDNO8GGxnWfg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/qs": {
+ "version": "6.9.6",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz",
+ "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA=="
+ },
+ "@types/range-parser": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
+ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
+ },
+ "@types/serve-static": {
+ "version": "1.13.9",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz",
+ "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==",
+ "requires": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "axios": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+ "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
+ "basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "requires": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
+ "create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz",
+ "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA=="
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "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=="
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz",
+ "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ=="
+ },
+ "mime-types": {
+ "version": "2.1.29",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz",
+ "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==",
+ "requires": {
+ "mime-db": "1.46.0"
+ }
+ },
+ "moment": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+ "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+ },
+ "morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "requires": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "requires": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
+ "ts-node": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
+ "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
+ "requires": {
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.17",
+ "yn": "3.1.1"
+ }
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "typescript": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
+ "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw=="
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..2e596a1
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "tsc; nodemon",
+ "dev": "nodemon ./index.ts"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@types/express": "^4.17.11",
+ "@types/node": "^14.14.34",
+ "axios": "^0.21.1",
+ "cors": "^2.8.5",
+ "express": "^4.17.1",
+ "moment": "^2.29.1",
+ "morgan": "^1.10.0",
+ "ts-node": "^9.1.1",
+ "typescript": "^4.2.3"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.10",
+ "@types/morgan": "^1.9.2",
+ "@types/node-sass": "^4.11.1"
+ }
+}
diff --git a/server/router.js b/server/router.js
index 65f69a4..4f66a7d 100644
--- a/server/router.js
+++ b/server/router.js
@@ -1,11 +1,11 @@
-const { Router } = require('express');
-const router = Router();
-const controller = require('./controller/controller');
-
-router.post('/tracks', controller.getTracks);
-router.post('/tokens', controller.getTokens);
-router.post('/artists', controller.getArtists);
-router.post('/create', controller.createPlaylist);
-router.post('/cover', controller.getPlaylistCover);
-
-module.exports = router;
\ No newline at end of file
+'use strict';
+Object.defineProperty(exports, '__esModule', { value: true });
+const express_1 = require('express');
+const router = express_1.Router();
+const controller_1 = require('./controller/controller');
+router.post('/tracks', controller_1.getTracks);
+router.post('/tokens', controller_1.getTokens);
+router.post('/artists', controller_1.getArtists);
+router.post('/create', controller_1.createPlaylist);
+router.post('/cover', controller_1.getPlaylistCover);
+exports.default = router;
diff --git a/server/router.ts b/server/router.ts
new file mode 100644
index 0000000..1d40b10
--- /dev/null
+++ b/server/router.ts
@@ -0,0 +1,11 @@
+import { Router } from "express";
+const router = Router();
+import { getTracks, getTokens, getArtists, createPlaylist, getPlaylistCover} from "./controller/controller";
+
+router.post("/tracks", getTracks);
+router.post("/tokens", getTokens);
+router.post("/artists", getArtists);
+router.post("/create", createPlaylist);
+router.post("/cover", getPlaylistCover);
+
+export default router;
diff --git a/server/tsconfig.json b/server/tsconfig.json
new file mode 100644
index 0000000..fe8a141
--- /dev/null
+++ b/server/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+ "strict": true, /* Enable all strict type-checking options. */
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ "skipLibCheck": true, /* Skip type checking of declaration files. */
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ }
+}