diff --git a/.gitignore b/.gitignore index a628adb..53bd674 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ dist npm-debug.log* yarn-debug.log* yarn-error.log* + +# cypress +cypress/screenshots +cypress/videos diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/cypress.json @@ -0,0 +1 @@ +{} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/fixtures/profile.json b/cypress/fixtures/profile.json new file mode 100644 index 0000000..b6c355c --- /dev/null +++ b/cypress/fixtures/profile.json @@ -0,0 +1,5 @@ +{ + "id": 8739, + "name": "Jane", + "email": "jane@example.com" +} \ No newline at end of file diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json new file mode 100644 index 0000000..79b699a --- /dev/null +++ b/cypress/fixtures/users.json @@ -0,0 +1,232 @@ +[ + { + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "address": { + "street": "Kulas Light", + "suite": "Apt. 556", + "city": "Gwenborough", + "zipcode": "92998-3874", + "geo": { + "lat": "-37.3159", + "lng": "81.1496" + } + }, + "phone": "1-770-736-8031 x56442", + "website": "hildegard.org", + "company": { + "name": "Romaguera-Crona", + "catchPhrase": "Multi-layered client-server neural-net", + "bs": "harness real-time e-markets" + } + }, + { + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", + "address": { + "street": "Victor Plains", + "suite": "Suite 879", + "city": "Wisokyburgh", + "zipcode": "90566-7771", + "geo": { + "lat": "-43.9509", + "lng": "-34.4618" + } + }, + "phone": "010-692-6593 x09125", + "website": "anastasia.net", + "company": { + "name": "Deckow-Crist", + "catchPhrase": "Proactive didactic contingency", + "bs": "synergize scalable supply-chains" + } + }, + { + "id": 3, + "name": "Clementine Bauch", + "username": "Samantha", + "email": "Nathan@yesenia.net", + "address": { + "street": "Douglas Extension", + "suite": "Suite 847", + "city": "McKenziehaven", + "zipcode": "59590-4157", + "geo": { + "lat": "-68.6102", + "lng": "-47.0653" + } + }, + "phone": "1-463-123-4447", + "website": "ramiro.info", + "company": { + "name": "Romaguera-Jacobson", + "catchPhrase": "Face to face bifurcated interface", + "bs": "e-enable strategic applications" + } + }, + { + "id": 4, + "name": "Patricia Lebsack", + "username": "Karianne", + "email": "Julianne.OConner@kory.org", + "address": { + "street": "Hoeger Mall", + "suite": "Apt. 692", + "city": "South Elvis", + "zipcode": "53919-4257", + "geo": { + "lat": "29.4572", + "lng": "-164.2990" + } + }, + "phone": "493-170-9623 x156", + "website": "kale.biz", + "company": { + "name": "Robel-Corkery", + "catchPhrase": "Multi-tiered zero tolerance productivity", + "bs": "transition cutting-edge web services" + } + }, + { + "id": 5, + "name": "Chelsey Dietrich", + "username": "Kamren", + "email": "Lucio_Hettinger@annie.ca", + "address": { + "street": "Skiles Walks", + "suite": "Suite 351", + "city": "Roscoeview", + "zipcode": "33263", + "geo": { + "lat": "-31.8129", + "lng": "62.5342" + } + }, + "phone": "(254)954-1289", + "website": "demarco.info", + "company": { + "name": "Keebler LLC", + "catchPhrase": "User-centric fault-tolerant solution", + "bs": "revolutionize end-to-end systems" + } + }, + { + "id": 6, + "name": "Mrs. Dennis Schulist", + "username": "Leopoldo_Corkery", + "email": "Karley_Dach@jasper.info", + "address": { + "street": "Norberto Crossing", + "suite": "Apt. 950", + "city": "South Christy", + "zipcode": "23505-1337", + "geo": { + "lat": "-71.4197", + "lng": "71.7478" + } + }, + "phone": "1-477-935-8478 x6430", + "website": "ola.org", + "company": { + "name": "Considine-Lockman", + "catchPhrase": "Synchronised bottom-line interface", + "bs": "e-enable innovative applications" + } + }, + { + "id": 7, + "name": "Kurtis Weissnat", + "username": "Elwyn.Skiles", + "email": "Telly.Hoeger@billy.biz", + "address": { + "street": "Rex Trail", + "suite": "Suite 280", + "city": "Howemouth", + "zipcode": "58804-1099", + "geo": { + "lat": "24.8918", + "lng": "21.8984" + } + }, + "phone": "210.067.6132", + "website": "elvis.io", + "company": { + "name": "Johns Group", + "catchPhrase": "Configurable multimedia task-force", + "bs": "generate enterprise e-tailers" + } + }, + { + "id": 8, + "name": "Nicholas Runolfsdottir V", + "username": "Maxime_Nienow", + "email": "Sherwood@rosamond.me", + "address": { + "street": "Ellsworth Summit", + "suite": "Suite 729", + "city": "Aliyaview", + "zipcode": "45169", + "geo": { + "lat": "-14.3990", + "lng": "-120.7677" + } + }, + "phone": "586.493.6943 x140", + "website": "jacynthe.com", + "company": { + "name": "Abernathy Group", + "catchPhrase": "Implemented secondary concept", + "bs": "e-enable extensible e-tailers" + } + }, + { + "id": 9, + "name": "Glenna Reichert", + "username": "Delphine", + "email": "Chaim_McDermott@dana.io", + "address": { + "street": "Dayna Park", + "suite": "Suite 449", + "city": "Bartholomebury", + "zipcode": "76495-3109", + "geo": { + "lat": "24.6463", + "lng": "-168.8889" + } + }, + "phone": "(775)976-6794 x41206", + "website": "conrad.com", + "company": { + "name": "Yost and Sons", + "catchPhrase": "Switchable contextually-based project", + "bs": "aggregate real-time technologies" + } + }, + { + "id": 10, + "name": "Clementina DuBuque", + "username": "Moriah.Stanton", + "email": "Rey.Padberg@karina.biz", + "address": { + "street": "Kattie Turnpike", + "suite": "Suite 198", + "city": "Lebsackbury", + "zipcode": "31428-2261", + "geo": { + "lat": "-38.2386", + "lng": "57.2232" + } + }, + "phone": "024-648-3804", + "website": "ambrose.net", + "company": { + "name": "Hoeger LLC", + "catchPhrase": "Centralized empowering task-force", + "bs": "target end-to-end models" + } + } +] \ No newline at end of file diff --git a/cypress/integration/examples/actions.spec.js b/cypress/integration/examples/actions.spec.js new file mode 100644 index 0000000..ea2a857 --- /dev/null +++ b/cypress/integration/examples/actions.spec.js @@ -0,0 +1,302 @@ +/// + +context('Actions', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/actions') + }) + + // https://on.cypress.io/interacting-with-elements + + it('.type() - type into a DOM element', () => { + // https://on.cypress.io/type + cy.get('.action-email') + .type('fake@email.com') + .should('have.value', 'fake@email.com') + + // .type() with special character sequences + .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') + .type('{del}{selectall}{backspace}') + + // .type() with key modifiers + .type('{alt}{option}') //these are equivalent + .type('{ctrl}{control}') //these are equivalent + .type('{meta}{command}{cmd}') //these are equivalent + .type('{shift}') + + // Delay each keypress by 0.1 sec + .type('slow.typing@email.com', { delay: 100 }) + .should('have.value', 'slow.typing@email.com') + + cy.get('.action-disabled') + // Ignore error checking prior to type + // like whether the input is visible or disabled + .type('disabled error checking', { force: true }) + .should('have.value', 'disabled error checking') + }) + + it('.focus() - focus on a DOM element', () => { + // https://on.cypress.io/focus + cy.get('.action-focus') + .focus() + .should('have.class', 'focus') + .prev() + .should('have.attr', 'style', 'color: orange;') + }) + + it('.blur() - blur off a DOM element', () => { + // https://on.cypress.io/blur + cy.get('.action-blur') + .type('About to blur') + .blur() + .should('have.class', 'error') + .prev() + .should('have.attr', 'style', 'color: red;') + }) + + it('.clear() - clears an input or textarea element', () => { + // https://on.cypress.io/clear + cy.get('.action-clear') + .type('Clear this text') + .should('have.value', 'Clear this text') + .clear() + .should('have.value', '') + }) + + it('.submit() - submit a form', () => { + // https://on.cypress.io/submit + cy.get('.action-form') + .find('[type="text"]') + .type('HALFOFF') + cy.get('.action-form') + .submit() + .next() + .should('contain', 'Your form has been submitted!') + }) + + it('.click() - click on a DOM element', () => { + // https://on.cypress.io/click + cy.get('.action-btn').click() + + // You can click on 9 specific positions of an element: + // ----------------------------------- + // | topLeft top topRight | + // | | + // | | + // | | + // | left center right | + // | | + // | | + // | | + // | bottomLeft bottom bottomRight | + // ----------------------------------- + + // clicking in the center of the element is the default + cy.get('#action-canvas').click() + + cy.get('#action-canvas').click('topLeft') + cy.get('#action-canvas').click('top') + cy.get('#action-canvas').click('topRight') + cy.get('#action-canvas').click('left') + cy.get('#action-canvas').click('right') + cy.get('#action-canvas').click('bottomLeft') + cy.get('#action-canvas').click('bottom') + cy.get('#action-canvas').click('bottomRight') + + // .click() accepts an x and y coordinate + // that controls where the click occurs :) + + cy.get('#action-canvas') + .click(80, 75) // click 80px on x coord and 75px on y coord + .click(170, 75) + .click(80, 165) + .click(100, 185) + .click(125, 190) + .click(150, 185) + .click(170, 165) + + // click multiple elements by passing multiple: true + cy.get('.action-labels>.label').click({ multiple: true }) + + // Ignore error checking prior to clicking + cy.get('.action-opacity>.btn').click({ force: true }) + }) + + it('.dblclick() - double click on a DOM element', () => { + // https://on.cypress.io/dblclick + + // Our app has a listener on 'dblclick' event in our 'scripts.js' + // that hides the div and shows an input on double click + cy.get('.action-div') + .dblclick() + .should('not.be.visible') + cy.get('.action-input-hidden').should('be.visible') + }) + + it('.check() - check a checkbox or radio element', () => { + // https://on.cypress.io/check + + // By default, .check() will check all + // matching checkbox or radio elements in succession, one after another + cy.get('.action-checkboxes [type="checkbox"]') + .not('[disabled]') + .check() + .should('be.checked') + + cy.get('.action-radios [type="radio"]') + .not('[disabled]') + .check() + .should('be.checked') + + // .check() accepts a value argument + cy.get('.action-radios [type="radio"]') + .check('radio1') + .should('be.checked') + + // .check() accepts an array of values + cy.get('.action-multiple-checkboxes [type="checkbox"]') + .check(['checkbox1', 'checkbox2']) + .should('be.checked') + + // Ignore error checking prior to checking + cy.get('.action-checkboxes [disabled]') + .check({ force: true }) + .should('be.checked') + + cy.get('.action-radios [type="radio"]') + .check('radio3', { force: true }) + .should('be.checked') + }) + + it('.uncheck() - uncheck a checkbox element', () => { + // https://on.cypress.io/uncheck + + // By default, .uncheck() will uncheck all matching + // checkbox elements in succession, one after another + cy.get('.action-check [type="checkbox"]') + .not('[disabled]') + .uncheck() + .should('not.be.checked') + + // .uncheck() accepts a value argument + cy.get('.action-check [type="checkbox"]') + .check('checkbox1') + .uncheck('checkbox1') + .should('not.be.checked') + + // .uncheck() accepts an array of values + cy.get('.action-check [type="checkbox"]') + .check(['checkbox1', 'checkbox3']) + .uncheck(['checkbox1', 'checkbox3']) + .should('not.be.checked') + + // Ignore error checking prior to unchecking + cy.get('.action-check [disabled]') + .uncheck({ force: true }) + .should('not.be.checked') + }) + + it('.select() - select an option in a element', () => { + // https://on.cypress.io/select + + // Select option(s) with matching text content + cy.get('.action-select').select('apples') + + cy.get('.action-select-multiple').select([ + 'apples', + 'oranges', + 'bananas' + ]) + + // Select option(s) with matching value + cy.get('.action-select').select('fr-bananas') + + cy.get('.action-select-multiple').select([ + 'fr-apples', + 'fr-oranges', + 'fr-bananas' + ]) + }) + + 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..53b9846 --- /dev/null +++ b/cypress/integration/examples/aliasing.spec.js @@ -0,0 +1,46 @@ +/// + +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.server() + cy.route('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('status') + .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..33c848c --- /dev/null +++ b/cypress/integration/examples/assertions.spec.js @@ -0,0 +1,173 @@ +/// + +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') + }) + }) +}) diff --git a/cypress/integration/examples/connectors.spec.js b/cypress/integration/examples/connectors.spec.js new file mode 100644 index 0000000..c9e89e3 --- /dev/null +++ b/cypress/integration/examples/connectors.spec.js @@ -0,0 +1,55 @@ +/// + +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') + }) + }) + + it('.then() - invoke 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') + }) + }) +}) diff --git a/cypress/integration/examples/cookies.spec.js b/cypress/integration/examples/cookies.spec.js new file mode 100644 index 0000000..9c3462a --- /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..0a2b072 --- /dev/null +++ b/cypress/integration/examples/cypress_api.spec.js @@ -0,0 +1,232 @@ +/// + +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({ + whitelist: 'session_id' + }) + }) +}) + +context('Cypress.Server', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/cypress-api') + }) + + // Permanently override server options for + // all instances of cy.server() + + // https://on.cypress.io/cypress-server + it('.defaults() - change default config of server', () => { + Cypress.Server.defaults({ + delay: 0, + force404: false + }) + }) +}) + +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('have.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..5a6bef8 --- /dev/null +++ b/cypress/integration/examples/files.spec.js @@ -0,0 +1,90 @@ +/// + +context('Files', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/files') + }) + 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. + + cy.server() + cy.fixture('example.json').as('comment') + cy.route('GET', 'comments/*', '@comment').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('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + + // you can also just write the fixture in the route + cy.route('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('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + + // or write fx to represent fixture + // by default it assumes it's .json + cy.route('GET', 'comments/*', 'fx:example').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('responseBody') + .should('have.property', 'name') + .and('include', 'Using fixtures to represent data') + }) + + it('cy.readFile() - read a files 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..9f15fcb --- /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..4f8ca45 --- /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..998a08f --- /dev/null +++ b/cypress/integration/examples/misc.spec.js @@ -0,0 +1,93 @@ +/// + +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', () => { + // https://on.cypress.io/exec + + // execute a system command. + // so you can take actions necessary for + // your test outside the scope of Cypress. + cy.exec('echo Jane Lane') + .its('stdout') + .should('contain', 'Jane Lane') + + // we can use Cypress.platform string to + // select appropriate command + // https://on.cypress/io/platform + cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) + + 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, + beforeScreenshot() {}, + afterScreenshot() {} + }) + }) + }) + + 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..e088bd5 --- /dev/null +++ b/cypress/integration/examples/navigation.spec.js @@ -0,0 +1,60 @@ +/// + +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..cf63ac7 --- /dev/null +++ b/cypress/integration/examples/network_requests.spec.js @@ -0,0 +1,150 @@ +/// + +context('Network Requests', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/network-requests') + }) + + // Manage AJAX / XHR requests in your app + + it('cy.server() - control behavior of network requests and responses', () => { + // https://on.cypress.io/server + + cy.server().should(server => { + // the default options on server + // you can override any of these options + expect(server.delay).to.eq(0) + expect(server.method).to.eq('GET') + expect(server.status).to.eq(200) + expect(server.headers).to.be.null + expect(server.response).to.be.null + expect(server.onRequest).to.be.undefined + expect(server.onResponse).to.be.undefined + expect(server.onAbort).to.be.undefined + + // These options control the server behavior + // affecting all requests + + // pass false to disable existing route stubs + expect(server.enable).to.be.true + // forces requests that don't match your routes to 404 + expect(server.force404).to.be.false + // whitelists requests from ever being logged or stubbed + expect(server.whitelist).to.be.a('function') + }) + + cy.server({ + method: 'POST', + delay: 1000, + status: 422, + response: {} + }) + + // any route commands will now inherit the above options + // from the server. anything we pass specifically + // to route will override the defaults though. + }) + + 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) + expect(response.body).to.have.length(500) + 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.length(500) + 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.route() - route responses to matching requests', () => { + // https://on.cypress.io/route + + let message = 'whoa, this comment does not exist' + + cy.server() + + // Listen to GET to comments/1 + cy.route('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('status') + .should('eq', 200) + + // Listen to POST to comments + cy.route('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') + + // get the route + cy.get('@postComment').should(xhr => { + expect(xhr.requestBody).to.include('email') + expect(xhr.requestHeaders).to.have.property('Content-Type') + expect(xhr.responseBody).to.have.property( + 'name', + 'Using POST in cy.route()' + ) + }) + + // Stub a response to PUT comments/ **** + cy.route({ + method: 'PUT', + url: 'comments/*', + status: 404, + response: { error: message }, + delay: 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..c08bbf4 --- /dev/null +++ b/cypress/integration/examples/querying.spec.js @@ -0,0 +1,87 @@ +/// + +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') + }) + }) +}) 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..d192645 --- /dev/null +++ b/cypress/integration/examples/spies_stubs_clocks.spec.js @@ -0,0 +1,72 @@ +/// + +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') + + let obj = { + foo() {} + } + + let spy = cy.spy(obj, 'foo').as('anyArgs') + + obj.foo() + + expect(spy).to.be.called + }) + + 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') + + let obj = { + /** + * prints both arguments to the console + * @param a {string} + * @param b {string} + */ + foo(a, b) { + console.log('a', a, 'b', b) + } + } + + let 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 + let 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 + let 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') + }) +}) diff --git a/cypress/integration/examples/traversal.spec.js b/cypress/integration/examples/traversal.spec.js new file mode 100644 index 0000000..7683471 --- /dev/null +++ b/cypress/integration/examples/traversal.spec.js @@ -0,0 +1,140 @@ +/// + +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..2e51756 --- /dev/null +++ b/cypress/integration/examples/utilities.spec.js @@ -0,0 +1,144 @@ +/// + +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 + 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.moment() - format or parse dates using a moment method', () => { + // https://on.cypress.io/moment + const time = Cypress.moment() + .utc('2014-04-25T19:38:53.196Z') + .format('h:mm A') + + expect(time).to.be.a('string') + + cy.get('.utility-moment') + .contains('3:38 PM') + .should('have.class', 'badge') + }) + + 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 + 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..23421eb --- /dev/null +++ b/cypress/integration/examples/viewport.spec.js @@ -0,0 +1,63 @@ +/// + +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..25d3c64 --- /dev/null +++ b/cypress/integration/examples/waiting.spec.js @@ -0,0 +1,35 @@ +/// + +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', () => { + cy.server() + + // Listen to GET to comments/1 + cy.route('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('status') + .should('eq', 200) + }) +}) diff --git a/cypress/integration/examples/window.spec.js b/cypress/integration/examples/window.spec.js new file mode 100644 index 0000000..d669b67 --- /dev/null +++ b/cypress/integration/examples/window.spec.js @@ -0,0 +1,24 @@ +/// + +context('Window', () => { + beforeEach(() => { + cy.visit('https://example.cypress.io/commands/window') + }) + + it('cy.window() - get the global window object', () => { + // https://on.cypress.io/window + cy.window().should('have.property', 'top') + }) + + it('cy.document() - get the document object', () => { + // https://on.cypress.io/document + cy.document() + .should('have.property', 'charset') + .and('eq', 'UTF-8') + }) + + it('cy.title() - get the title', () => { + // https://on.cypress.io/title + cy.title().should('include', 'Kitchen Sink') + }) +}) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..19b9052 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// 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) + +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..c1f5a77 --- /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 is 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/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000..0cbfffb --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "strict": true, + "baseUrl": "../node_modules", + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress"] + }, + "include": ["**/*.ts"] +} diff --git a/package.json b/package.json index 49c7f1c..e44bf43 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "concurrently \"cd client && npm start\" \"cd server && npm start\"", "build": "./scripts/build.sh", "test": "npm run lint && scripts/test.sh", + "test:e2e": "npm start & wait-on http://localhost:3000 & wait-on http://localhost:4000 && npm run cypress:run", "test:watch": "npm run lint && concurrently \"cd client && npm run test:watch\" \"cd server && npm run test:watch\"", "format:js": "prettier --parser typescript --write '**/*.{js,jsx,ts,tsx}'", "format:css": "prettier-stylelint --ignore-path .gitignore --write --quiet '**/*.{css,scss}'", @@ -18,6 +19,8 @@ "lint": "npm run lint:js && npm run lint:css", "release": "release-it --non-interactive", "changelog": "auto-changelog -p" + "cypress:run": "cypress run", + "cypress:open": "cypress open" }, "dependencies": {}, "devDependencies": { @@ -26,6 +29,7 @@ "@typescript-eslint/parser": "^1.7.0", "auto-changelog": "^1.13.0", "concurrently": "^4.1.0", + "cypress": "^3.2.0", "eslint": "^5.16.0", "eslint-config-oceanprotocol": "^1.3.0", "eslint-config-prettier": "^4.2.0", @@ -38,6 +42,7 @@ "stylelint-config-css-modules": "^1.4.0", "stylelint-config-standard": "^18.3.0", "typescript": "3.4.5" + "wait-on": "^3.2.0" }, "repository": { "type": "git", diff --git a/scripts/test.sh b/scripts/test.sh index bfd836a..78c67e3 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -10,3 +10,7 @@ do npm test cd .. done + +echo "Running End-to-End tests" + +npm run test:e2e