diff --git a/.travis.yml b/.travis.yml index b7a9a2bcd2..2cfc182e6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ stages: - <<: *stage name: test - <<: *stage - name: cypress-tests + name: cypress-tests:smoke - <<: *stage name: build - name: deploy @@ -35,6 +35,12 @@ stages: if: tag =~ ^v\d+ - <<: *stage name: sauce-tests + - <<: *stage + name: cypress-tests:full:herokuapp + if: branch = development AND type = push OR tag =~ ^v\d+ + - <<: *stage + name: cypress-tests:full:gh-pages + if: branch = development AND type = push OR tag =~ ^v\d+ before_install: - sh -e /etc/init.d/xvfb start @@ -87,11 +93,11 @@ jobs: # - <<: *test # env: NGV=next -# test cypress - - stage: cypress-tests +# test cypress smoke + - stage: cypress-tests:smoke env: URL=localhost:4200/# before_script: ng serve --prod & - script: $(npm bin)/wait-on http-get://$URL && npm run cy:run + script: $(npm bin)/wait-on http-get://$URL && npm run cy:run:smoke # check prod build - &build @@ -163,6 +169,16 @@ jobs: on: tags: true +# test cypress full for herokuapp + - stage: cypress-tests:full:herokuapp + env: URL=https://ngx-universal.herokuapp.com/# + script: npm run cy:run:smoke && npm run cy:run:full + +# test cypress full for gh-pages + - stage: cypress-tests:full:gh-pages + env: URL=https://valor-software.com/ngx-bootstrap/#/ + script: npm run cy:run:smoke && npm run cy:run:full + cache: apt: true npm: true diff --git a/cypress.json b/cypress.json index 35631076f0..57307cfba2 100644 --- a/cypress.json +++ b/cypress.json @@ -1,3 +1,4 @@ { - "baseUrl": "http://localhost:4200/#" + "baseUrl": "http://localhost:4200/#", + "video": false } diff --git a/cypress/full/carousel_page_spec.ts b/cypress/full/carousel_page_spec.ts new file mode 100644 index 0000000000..68ad15fc30 --- /dev/null +++ b/cypress/full/carousel_page_spec.ts @@ -0,0 +1,316 @@ +import { CarouselPo } from '../support/carousel.po'; + +describe('Carousel page test suite', () => { + const carousel = new CarouselPo(); + + beforeEach(() => { + cy.clock(); + carousel.navigateTo(); + }); + + describe('Basic', () => { + const basic = carousel.exampleDemosArr.basic; + + it('when user click on indicator item - appropriate slide shown', () => { + carousel.isClickActivatedCarouselItem(basic, 1); + carousel.isClickActivatedCarouselItem(basic, 0); + carousel.isClickActivatedCarouselItem(basic, 2); + }); + + it('when user click on left/right arrow - previous/next slide shown', () => { + carousel.clickOnCtrl(basic, 'left'); + carousel.isCarouselItemActive(basic, 2); + carousel.clickOnCtrl(basic, 'right'); + carousel.isCarouselItemActive(basic, 0); + }); + + it('when user do nothing more than 5 sec - next slide automatically shown', () => { + carousel.scrollToMenu('Basic'); + carousel.isCarouselItemActive(basic, 0); + cy.tick(6000); + carousel.isCarouselItemActive(basic, 1); + }); + }); + + describe('Optional captions', () => { + const optCaptions = carousel.exampleDemosArr.optionalCaptions; + + it('example contains slides, indicators, left and right controls and captions', () => { + carousel.scrollToMenu('Optional captions'); + carousel.isCarouselHaveIndicatorsItemsCtrls(optCaptions); + carousel.isEachSlideHave(optCaptions, ['.item', '.carousel-caption', 'h3']); + }); + + it('when user click on indicator item - appropriate slide shown', () => { + carousel.isClickActivatedCarouselItem(optCaptions, 1); + carousel.isClickActivatedCarouselItem(optCaptions, 0); + carousel.isClickActivatedCarouselItem(optCaptions, 2); + }); + + it('when user click on left/right arrow - previous/next slide shown', () => { + carousel.scrollToMenu('Optional captions'); + carousel.clickOnCtrl(optCaptions, 'left'); + carousel.isCarouselItemActive(optCaptions, 2); + carousel.clickOnCtrl(optCaptions, 'right'); + carousel.isCarouselItemActive(optCaptions, 0); + }); + + it('when user do nothing more than 5 sec - next slide automatically shown', () => { + carousel.scrollToMenu('Optional captions'); + carousel.isCarouselItemActive(optCaptions, 0); + cy.tick(6000); + carousel.isCarouselItemActive(optCaptions, 1); + }); + }); + + describe('Configuring defaults', () => { + const confDefaults = carousel.exampleDemosArr.configuringDefaults; + + it('example contains slides, indicators, left and right controls and captions', () => { + carousel.scrollToMenu('Configuring defaults'); + carousel.isCarouselHaveIndicatorsItemsCtrls(confDefaults); + carousel.isEachSlideHave(confDefaults, ['.item', '.carousel-caption', 'h3']); + }); + + it('when user click on indicator item - appropriate slide shown', () => { + carousel.isClickActivatedCarouselItem(confDefaults, 1); + carousel.isClickActivatedCarouselItem(confDefaults, 0); + carousel.isClickActivatedCarouselItem(confDefaults, 2); + }); + + it('when user do nothing more than 1.5 sec - next slide automatically shown', () => { + carousel.scrollToMenu('Optional captions'); + carousel.isCarouselItemActive(confDefaults, 0); + cy.tick(2000); + carousel.isCarouselItemActive(confDefaults, 1); + }); + }); + + describe('Dynamic Slides ', () => { + const dynamicSlides = carousel.exampleDemosArr.dynamicSlides; + + it('example contains slides, indicators, left and right controls and captions', () => { + carousel.scrollToMenu('Dynamic Slides'); + carousel.isCarouselHaveIndicatorsItemsCtrls(dynamicSlides); + carousel.isEachSlideHave(dynamicSlides, ['.item', '.carousel-caption', 'h4']); + }); + + it('example contains 3 additional buttons: "Add Slide", "Remove Current", "Remove #3"', () => { + carousel.scrollToMenu('Dynamic Slides'); + carousel.isBtnTxtEqual(dynamicSlides, 'Add Slide ', 0); + carousel.isBtnTxtEqual(dynamicSlides, 'Remove Current ', 1); + carousel.isBtnTxtEqual(dynamicSlides, 'Remove #3 ', 2); + }); + + it('when user click on "Add Slide", then amount of slides increased at 1 with header and info', () => { + carousel.scrollToMenu('Dynamic Slides'); + carousel.isSlidesCountEqual(dynamicSlides, 4); + carousel.clickOnBtn(dynamicSlides, 0); + carousel.isSlidesCountEqual(dynamicSlides, 5); + carousel.clickOnBtn(dynamicSlides, 0); + carousel.isSlidesCountEqual(dynamicSlides, 6); + }); + + it('when user click on "Remove Current", then amount of slides decreased at 1 and current slide deleted', () => { + carousel.scrollToMenu('Dynamic Slides'); + carousel.isSlidesCountEqual(dynamicSlides, 4); + carousel.clickOnBtn(dynamicSlides, 1); + carousel.isSlidesCountEqual(dynamicSlides, 3); + carousel.clickOnBtn(dynamicSlides, 1); + carousel.isSlidesCountEqual(dynamicSlides, 2); + }); + + it('when user click on "Remove #3" - then third slide deleted', () => { + carousel.scrollToMenu('Dynamic Slides'); + carousel.isSlidesCountEqual(dynamicSlides, 4); + carousel.clickOnBtn(dynamicSlides, 2); + carousel.isSlidesCountEqual(dynamicSlides, 3); + carousel.clickOnBtn(dynamicSlides, 2); + carousel.isSlidesCountEqual(dynamicSlides, 2); + carousel.clickOnBtn(dynamicSlides, 2); + carousel.isSlidesCountEqual(dynamicSlides, 2); + }); + + it('when user do nothing more than 5 sec - next slide automatically shown', () => { + carousel.scrollToMenu('Optional captions'); + carousel.isCarouselItemActive(dynamicSlides, 0); + cy.tick(6000); + carousel.isCarouselItemActive(dynamicSlides, 1); + }); + }); + + describe('Pause on hover ', () => { + const pauseOnHoverSlides = carousel.exampleDemosArr.pauseOnHover; + + it('example contains carousel component with slides, arrows and "Toggle pause on hover" button', () => { + carousel.scrollToMenu('Pause on hover'); + carousel.isCarouselHaveIndicatorsItemsCtrls(pauseOnHoverSlides); + carousel.isEachSlideHave(pauseOnHoverSlides, ['.item', '.carousel-caption', 'h3']); + carousel.isBtnTxtEqual(pauseOnHoverSlides, 'Toggle pause on hover '); + }); + + it('when user click on "Toggle pause on hover" and hover slide - then after 5 sec slide stay opened', () => { + carousel.scrollToMenu('Pause on hover'); + carousel.clickOnBtn(pauseOnHoverSlides); + carousel.hoverSlide(pauseOnHoverSlides, 1); + carousel.isCarouselItemActive(pauseOnHoverSlides, 0); + cy.tick(6000); + carousel.isCarouselItemActive(pauseOnHoverSlides, 0); + }); + + it('when user click on "Toggle pause on hover" again, hover slide - then after 5 sec slide changed', () => { + carousel.scrollToMenu('Pause on hover'); + carousel.dblClickOnBtn(pauseOnHoverSlides); + carousel.hoverSlide(pauseOnHoverSlides, 1); + carousel.isCarouselItemActive(pauseOnHoverSlides, 0); + cy.tick(6000); + carousel.isCarouselItemActive(pauseOnHoverSlides, 1); + }); + }); + + describe('Custom content ', () => { + const customContentSlides = carousel.exampleDemosArr.customContent; + + it('example contains slides, indicators, left and right controls and custom content', () => { + carousel.scrollToMenu('Custom content'); + carousel.isCarouselHaveIndicatorsItemsCtrls(customContentSlides); + carousel.isEachSlideHave(customContentSlides, ['.item', 'h3', 'p', '.lead', 'h2']); + }); + }); + + describe('Disable slide looping ', () => { + const disableLoopingSlides = carousel.exampleDemosArr.disableLooping; + + it('example contains slides, indicators, left and right controls and "Disable Slide Looping" checkbox', () => { + carousel.scrollToMenu('Disable slide looping'); + carousel.isCarouselHaveIndicatorsItemsCtrls(disableLoopingSlides); + carousel.isLabelTxtEqual(disableLoopingSlides, 'Disable Slide Looping'); + }); + + it('when user click on checkbox "Disable Slide Looping", then no one slide should be shown after latest', () => { + carousel.scrollToMenu('Disable slide looping'); + carousel.isClickActivatedCarouselItem(disableLoopingSlides, 2); + carousel.clickOnInput(disableLoopingSlides); + cy.tick(6000); + carousel.isCarouselItemActive(disableLoopingSlides, 2); + }); + + it('when user uncheck "Disable slide looping", then slides continue changing after 5 sec', () => { + carousel.scrollToMenu('Disable slide looping'); + carousel.isClickActivatedCarouselItem(disableLoopingSlides, 2); + carousel.dblClickOnInput(disableLoopingSlides); + cy.tick(6000); + carousel.isCarouselItemActive(disableLoopingSlides, 0); + }); + }); + + describe('Disable indicator ', () => { + const disableIndicatorSlides = carousel.exampleDemosArr.disableIndicator; + + it('example contains slides, indicators, left and right controls and "Enable/Disable" button', () => { + carousel.scrollToMenu('Disable indicator'); + carousel.isCarouselHaveIndicatorsItemsCtrls(disableIndicatorSlides); + carousel.isBtnTxtEqual(disableIndicatorSlides, 'Enable/Disable Indicator '); + }); + it('when user click on "Enable/Disable Indicator" - indicator disappeared', () => { + carousel.scrollToMenu('Disable indicator'); + carousel.clickOnBtn(disableIndicatorSlides); + carousel.isCarouselIndicatorDisabled(disableIndicatorSlides, true); + }); + it('when user click on "Enable/Disable Indicator" again - indicator appeared', () => { + carousel.scrollToMenu('Disable indicator'); + carousel.dblClickOnBtn(disableIndicatorSlides); + carousel.isCarouselIndicatorDisabled(disableIndicatorSlides, false); + }); + }); + + describe('Interval ', () => { + const intervalSlides = carousel.exampleDemosArr.interval; + + it('example contains slides, indicators, left and right controls and input with default interval: 1500', () => { + carousel.scrollToMenu('Interval'); + carousel.isCarouselHaveIndicatorsItemsCtrls(intervalSlides); + carousel.isInputHaveAttrs(intervalSlides, + [{ attr: 'type', value: 'number' }, { attr: 'ng-reflect-model', value: '1500' }]); + }); + + it('when user change the interval in input to any positive value - then slides change after added interval', () => { + carousel.scrollToMenu('Interval'); + const newInterval = '3000'; + carousel.clearInputAndSendKeys(intervalSlides, newInterval); + carousel.isInputHaveAttrs(intervalSlides, + [{ attr: 'type', value: 'number' }, { attr: 'ng-reflect-model', value: newInterval }]); + carousel.isCarouselItemActive(intervalSlides, 0); + cy.tick(4000); + carousel.isCarouselItemActive(intervalSlides, 1); + }); + + it('When user change the interval in input to "0" - then slider stopped', () => { + carousel.scrollToMenu('Interval'); + const newInterval = '0'; + carousel.clearInputAndSendKeys(intervalSlides, newInterval); + carousel.isInputHaveAttrs(intervalSlides, + [{ attr: 'type', value: 'number' }, { attr: 'ng-reflect-model', value: newInterval }]); + carousel.isCarouselItemActive(intervalSlides, 0); + cy.tick(3000); + carousel.isCarouselItemActive(intervalSlides, 0); + }); + + it('When user change the interval in input to any negative value - then carousel stopped', () => { + carousel.scrollToMenu('Interval'); + const newInterval = '-100'; + carousel.clearInputAndSendKeys(intervalSlides, newInterval); + carousel.isInputHaveAttrs(intervalSlides, + [{ attr: 'type', value: 'number' }, { attr: 'ng-reflect-model', value: newInterval }]); + carousel.isCarouselItemActive(intervalSlides, 0); + cy.tick(3000); + carousel.isCarouselItemActive(intervalSlides, 0); + }); + }); + + describe('Slide changed event ', () => { + const changedEventSlides = carousel.exampleDemosArr.slideChangedEvent; + + it('example contains slides, indicators, left and right controls and "Slide has been switched: 0"', () => { + carousel.scrollToMenu('Slide changed event'); + carousel.isCarouselHaveIndicatorsItemsCtrls(changedEventSlides); + carousel.isCardTxtEqual(changedEventSlides, 'Slide has been switched: 0'); + }); + + it('when user click on left arrow - info changed to "Slide has been switched: 2"', () => { + carousel.scrollToMenu('Slide changed event'); + carousel.clickOnCtrl(changedEventSlides, 'left'); + carousel.isCardTxtEqual(changedEventSlides, 'Slide has been switched: 2'); + }); + + it('when user click on left arrow again - info changed to "Slide has been switched: 1"', () => { + carousel.scrollToMenu('Slide changed event'); + carousel.clickOnCtrl(changedEventSlides, 'left'); + carousel.clickOnCtrl(changedEventSlides, 'left'); + carousel.isCardTxtEqual(changedEventSlides, 'Slide has been switched: 1'); + }); + + it('when user click on right arrow - info changed to "Slide has been switched: 1"', () => { + carousel.scrollToMenu('Slide changed event'); + carousel.clickOnCtrl(changedEventSlides, 'right'); + carousel.isCardTxtEqual(changedEventSlides, 'Slide has been switched: 1'); + }); + + it('when user click on right arrow again - info changed to "Slide has been switched: 2"', () => { + carousel.scrollToMenu('Slide changed event'); + carousel.clickOnCtrl(changedEventSlides, 'right'); + carousel.clickOnCtrl(changedEventSlides, 'right'); + carousel.isCardTxtEqual(changedEventSlides, 'Slide has been switched: 2'); + }); + }); + + describe('Accessibility ', () => { + const accessibilityInfo = carousel.exampleDemosArr.accessibility; + + it('example contains info about "alt" attribute', () => { + cy.viewport(1440, 900); + carousel.clickOnDemoMenu('Accessibility'); + carousel.isDemoContainsTxt(accessibilityInfo, 'alt'); + }); + }); +}); diff --git a/cypress/integration/carousel_page_spec.ts b/cypress/integration/carousel_page_spec.ts index 7984aae0fd..d065cac6f9 100644 --- a/cypress/integration/carousel_page_spec.ts +++ b/cypress/integration/carousel_page_spec.ts @@ -9,11 +9,7 @@ describe('Carousel page test suite', () => { const basic = carousel.exampleDemosArr.basic; it('example contains slides, indicators, left and right controls', () => { - cy.get(`${ basic } ${ carousel.carouselClass }`) - .should('to.have.descendants', carousel.indicatorClass) - .and('to.have.descendants', carousel.itemClass) - .and('to.have.descendants', carousel.leftControl) - .and('to.have.descendants', carousel.rightControl); + carousel.isCarouselHaveIndicatorsItemsCtrls(basic); }); }); }); diff --git a/cypress/support/base.component.ts b/cypress/support/base.component.ts index 47c90574cb..a0554afa35 100644 --- a/cypress/support/base.component.ts +++ b/cypress/support/base.component.ts @@ -1,3 +1,5 @@ +import { AttrObj } from './interfaces'; + export abstract class BaseComponent { titleSel = 'h1'; titleLinkSel = '.content-header a'; @@ -10,7 +12,67 @@ export abstract class BaseComponent { cy.visit(this.pageUrl); } + scrollToMenu(subMenu: string) { + cy.get('examples h3').contains(subMenu).scrollIntoView(); + } + + clickOnDemoMenu(subMenu: string) { + cy.get('add-nav').contains('a', subMenu).click(); + } + clickByText(parent: string, text: string) { cy.get(parent).contains(text).click(); } + + dblClickByText(parent: string, text: string) { + cy.get(parent).contains(text).dblclick(); + } + + isBtnTxtEqual(baseSelector: string, expectedBtnTxt: string, buttonIndex?: number) { + cy.get(`${ baseSelector } button`).eq(buttonIndex ? buttonIndex : 0).invoke('text') + .should(btnTxt => expect(btnTxt).to.equal(expectedBtnTxt)); + } + + isLabelTxtEqual(baseSelector: string, expectedLabelTxt: string, labelIndex?: number) { + cy.get(`${baseSelector} label`).eq(labelIndex ? labelIndex : 0).invoke('text') + .should(labelTxt => expect(labelTxt).to.equal(expectedLabelTxt)); + } + + clickOnBtn(baseSelector: string, buttonIndex?: number) { + cy.get(`${ baseSelector } button`).eq(buttonIndex ? buttonIndex : 0).click(); + } + + dblClickOnBtn(baseSelector: string, buttonIndex?: number) { + cy.get(`${ baseSelector } button`).eq(buttonIndex ? buttonIndex : 0).dblclick(); + } + + clickOnInput(baseSelector: string, inputIndex?: number) { + cy.get(`${ baseSelector } input`).eq(inputIndex ? inputIndex : 0).click(); + } + + dblClickOnInput(baseSelector: string, inputIndex?: number) { + cy.get(`${ baseSelector } input`).eq(inputIndex ? inputIndex : 0).dblclick(); + } + + isInputHaveAttrs(baseSelector: string, attributes: AttrObj[]) { + cy.get(`${baseSelector} input`) + .then(input => { + let i = 0; + for (; i < attributes.length; i++) { + expect(input).to.have.attr(attributes[i].attr, attributes[i].value); + } + }); + } + + clearInputAndSendKeys(baseSelector: string, dataToSend: string) { + cy.get(`${baseSelector} input`).clear().type(dataToSend); + } + + isDemoContainsTxt(baseSelector: string, expectedTxt: string, expectedTxtOther?: string) { + cy.get(`${baseSelector}`).invoke('text') + .should(blockTxt => { + expect(blockTxt).to.contains(expectedTxt); + expect(blockTxt).to.contains(expectedTxtOther ? expectedTxtOther : expectedTxt); + }); + } } diff --git a/cypress/support/carousel.po.ts b/cypress/support/carousel.po.ts index 42805e7304..9d7b580798 100644 --- a/cypress/support/carousel.po.ts +++ b/cypress/support/carousel.po.ts @@ -12,6 +12,70 @@ export class CarouselPo extends BaseComponent { rightControl = '.carousel-control-next'; exampleDemosArr = { - basic: 'demo-carousel-basic' + basic: 'demo-carousel-basic', + optionalCaptions: 'demo-carousel-captions', + configuringDefaults: 'demo-carousel-config', + dynamicSlides: 'demo-carousel-dynamic', + pauseOnHover: 'demo-carousel-no-pause', + customContent: 'demo-carousel-custom-content', + disableLooping: 'demo-carousel-disable-looping', + disableIndicator: 'demo-carousel-disable-indicator', + interval: 'demo-carousel-interval', + slideChangedEvent: 'demo-carousel-slide-changed-event', + accessibility: 'demo-accessibility' }; + + isClickActivatedCarouselItem(baseSelector: string, itemIndex: number) { + cy.get(`${baseSelector} ${this.carouselClass} ${this.indicatorClass} li `) + .eq(itemIndex) + .click() + .should('have.class', 'active'); + } + + isCarouselItemActive(baseSelector: string, itemIndex: number) { + cy.get(`${baseSelector} ${this.carouselClass} ${this.indicatorClass} li `) + .eq(itemIndex) + .should('have.class', 'active'); + } + + clickOnCtrl(baseSelector: string, ctrlType: string) { + cy.get(`${baseSelector} ${ctrlType === 'left' ? this.leftControl : this.rightControl}`).click(); + } + + isCarouselHaveIndicatorsItemsCtrls(baseSelector: string) { + cy.get(`${baseSelector} ${this.carouselClass}`) + .should('to.have.descendants', this.indicatorClass) + .and('to.have.descendants', this.itemClass) + .and('to.have.descendants', this.leftControl) + .and('to.have.descendants', this.rightControl); + } + + isEachSlideHave(baseSelector: string, slideParams: string[]) { + cy.get(`${baseSelector} ${this.carouselClass} slide `) + .each(slide => { + let i = 0; + for (; i < slideParams.length; i++) { + expect(slide).to.have.descendants(slideParams[i]); + }}); + } + + isSlidesCountEqual(baseSelector: string, expectedCount: number) { + cy.get(`${baseSelector} ${this.indicatorClass} li`) + .should('to.have.length', expectedCount); + } + + isCardTxtEqual(baseSelector: string, expectedTxt: string) { + cy.get(`${baseSelector} .card.card-block`).invoke('text') + .should(blockTxt => expect(blockTxt).to.equal(expectedTxt)); + } + + hoverSlide(baseSelector: string, slideIndex: number) { + cy.get(`${baseSelector} ${this.carouselClass} div`).eq(slideIndex).trigger('mouseenter'); + } + + isCarouselIndicatorDisabled(baseSelector: string, disabled: boolean) { + cy.get(`${baseSelector} ${this.carouselClass}`) + .should(disabled ? 'to.not.have.descendants' : 'to.have.descendants', this.indicatorClass) + .and('to.have.descendants', this.itemClass); + } } diff --git a/cypress/support/interfaces.ts b/cypress/support/interfaces.ts new file mode 100644 index 0000000000..b4d1dcdc4a --- /dev/null +++ b/cypress/support/interfaces.ts @@ -0,0 +1,4 @@ +export interface AttrObj { + attr: string; + value: string; +} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index e51fcaf333..b954296b9f 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -14,6 +14,7 @@ }, "include": [ "integration/*.ts", + "full/*.ts", "support/*.ts", "../node_modules/cypress" ] diff --git a/package.json b/package.json index e8980374a9..7d86d377f1 100755 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "git-commit-changelog": "git commit -am \"chore(changelog): update [skip ci] \"", "changelog": "conventional-changelog -i CHANGELOG.md -s -p angular -r 2", "cy:open": "cypress open", - "cy:run": "cypress run --config video=false", + "cy:run:smoke": "cypress run --config integrationFolder=cypress/integration", + "cy:run:full": "cypress run --config integrationFolder=cypress/full", "view-stats": "webpack-bundle-analyzer demo/dist/stats.json", "build:dynamic": "run-s build build:client-and-server-bundles webpack:server configure-heroku", "serve:dynamic": "node demo/dist/server",