From fe530319099d0e2ad537b9b0070d92d8fdb359ec Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 13 Sep 2017 23:42:42 -0400 Subject: [PATCH 01/10] Fix e2e (integration) tests - Upgrade protractor & dependencies - Rewrite tests to match new APIs - Remove deprecated tests - Remove protractor-mac.conf.js & use a single protractor config - Add `user logs in` spec to work out bugs with the login flow. - Fix login flow (non-angular page), update some helpers to new protractor apis, fixing individual tests in progress - Add temp e2e grunt task to make testing quicker - Add additional scrollTo window helpers - Update createFromTemplate page to eliminate test flakes due to protractor changes - Add nextSteps page pageObject - Remove chrome from multiCapabilities in protractor.conf so grunt can handle setting the browser - Update framework from jasmine to jasmine2 in protractor.conf.js - Deduplicate common setup across test suites - Add implicit browser.wait(1000) every time new PageObject() or PageObject.visit() to ensure items are properly rendered before tests continue - Update support for screenshots. Screenshots will be saved in /test/tmp//.png - Screenshots taken for failed tests only - Add new user_creates_project e2e test that exercises our menus (rather than browser page refreshes) - Always default protractor to run with Chrome - Standardize timing across modules - Improve reporting output of protractor tests - Use color output locally on mac for test output, plain text in Jenkins - Jenkins does not do well with the escape codes - Prow (Jenkins) should now use the JUnit xml files to give us better context on failure anyway --- .travis.yml | 18 +- Gruntfile.js | 89 ++-- Makefile | 2 +- package.json | 11 +- test/.jshintrc | 1 + test/integration/e2e.js | 77 ---- test/integration/environment.js | 4 + .../user_adds_imagestream_to_project.spec.js | 18 +- .../user_adds_template_to_project.spec.js | 42 +- .../features/user_creates_from_url.spec.js | 58 ++- .../features/user_creates_project.spec.js | 306 ++++---------- .../integration/features/user_logs_in.spec.js | 25 ++ test/integration/helpers.js | 19 - test/integration/helpers/common.js | 31 ++ test/integration/helpers/forms.js | 23 + test/integration/helpers/hasAngularDefined.js | 22 + test/integration/helpers/inputs.js | 11 +- test/integration/helpers/matchers.js | 43 +- test/integration/helpers/nonAngular.js | 18 + test/integration/helpers/timing.js | 24 ++ test/integration/helpers/wait.js | 16 + test/integration/helpers/waitForRedirect.js | 20 + test/integration/helpers/window.js | 41 ++ .../page-objects/createFromTemplate.js | 20 +- .../integration/page-objects/createFromURL.js | 15 +- .../integration/page-objects/createProject.js | 25 +- .../{catalog.js => legacyCatalog.js} | 17 +- test/integration/page-objects/login.js | 30 ++ test/integration/page-objects/menus.js | 111 ++++- .../page-objects/modals/addTemplateModal.js | 5 +- test/integration/page-objects/nextSteps.js | 18 + test/integration/page-objects/page.js | 14 +- test/integration/page-objects/projectList.js | 30 ++ test/karma.conf.js | 3 +- test/protractor-mac.conf.js | 19 - test/protractor.conf.js | 392 ++++-------------- 36 files changed, 804 insertions(+), 814 deletions(-) delete mode 100644 test/integration/e2e.js create mode 100644 test/integration/environment.js create mode 100644 test/integration/features/user_logs_in.spec.js create mode 100644 test/integration/helpers/common.js create mode 100644 test/integration/helpers/forms.js create mode 100644 test/integration/helpers/hasAngularDefined.js create mode 100644 test/integration/helpers/nonAngular.js create mode 100644 test/integration/helpers/timing.js create mode 100644 test/integration/helpers/wait.js create mode 100644 test/integration/helpers/waitForRedirect.js create mode 100644 test/integration/helpers/window.js rename test/integration/page-objects/{catalog.js => legacyCatalog.js} (80%) create mode 100644 test/integration/page-objects/login.js create mode 100644 test/integration/page-objects/nextSteps.js create mode 100644 test/integration/page-objects/projectList.js delete mode 100644 test/protractor-mac.conf.js diff --git a/.travis.yml b/.travis.yml index 15affdd7df..17563d7c7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,16 @@ language: node_js node_js: - "6" +before_install: + # virtual frame buffer for headless browsers + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - sleep 3 # give xvfb some time to start before_script: - - make build - # FIXME: - # This needs to be enabled again when we eliminate PhantomJS for Firefox - # Even the unit tests require a browser - # - export DISPLAY=:99.0 - # - sh -e /etc/init.d/xvfb start - # - sleep 3 # give xvfb some time to start + - make build script: - - grunt test - hack/verify-dist.sh + - grunt test-unit + # There is no server running in travis, therefore + # we cannot run the e2e tests. + # - grunt test-integration --browser=chrome diff --git a/Gruntfile.js b/Gruntfile.js index ed1a08179c..0bbfa749a3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,10 +11,21 @@ var modRewrite = require('connect-modrewrite'); var serveStatic = require('serve-static'); +var capitalize = (str) => { + return str.charAt(0).toUpperCase() + str.substr(1); +}; + module.exports = function (grunt) { var contextRoot = grunt.option('contextRoot') || "dev-console"; var isMac = /^darwin/.test(process.platform) || grunt.option('mac'); - + var specifiedUnitTestBrowsers = grunt.option('browsers') && + grunt.option('browsers').split(',').map(capitalize); + // Travis & Jenkins expect Chrome + var defaultUnitTestBrowsers = isMac ? ['Firefox', 'Chrome'] : ['Chrome']; + var unitTestBrowsers = specifiedUnitTestBrowsers ? + specifiedUnitTestBrowsers : + defaultUnitTestBrowsers; + var integrationTestBrowser = grunt.option('browser') || 'chrome'; // Load grunt tasks automatically require('load-grunt-tasks')(grunt, { @@ -619,15 +630,7 @@ module.exports = function (grunt) { // grunt test // grunt test --browsers=Chrome // grunt test --browsers=Chrome,Firefox,Safari (be sure karma--launcher is installed) - browsers: grunt.option('browsers') ? - grunt.option('browsers').split(',') : - // if running locally on mac, we can test both FF & Chrome, - // in Travis, just FF - // ['Nightmare'] is a good alt for a current headless - // FIXME: fix this, PhantomJS is deprecated - isMac ? - ['Firefox', 'Chrome'] : - ['PhantomJS'] + browsers: unitTestBrowsers }, unit: { singleRun: true, @@ -641,7 +644,8 @@ module.exports = function (grunt) { noColor: false, args: { suite: grunt.option('suite') || 'full', - baseUrl: grunt.option('baseUrl') || ("https://localhost:9000/" + contextRoot + "/") + baseUrl: grunt.option('baseUrl') || ("https://localhost:9000/" + contextRoot + "/"), + browser: integrationTestBrowser } }, default: { @@ -649,16 +653,7 @@ module.exports = function (grunt) { configFile: "test/protractor.conf.js", args: { baseUrl: grunt.option('baseUrl') || ("https://localhost:9000/" + contextRoot + "/"), - browser: grunt.option('browser') || "firefox" - } - } - }, - mac: { - options: { - configFile: "test/protractor-mac.conf.js", - args: { - baseUrl: grunt.option('baseUrl') || ("https://localhost:9000/" + contextRoot + "/"), - browser: grunt.option('browser') || "firefox" + browser: integrationTestBrowser } } } @@ -744,34 +739,52 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-angular-templates'); + // ex: + // grunt test-unit + // grunt test-unit --skipRebuild (to save time between test runs) // karma must run prior to coverage since karma will generate the coverage results - grunt.registerTask('test-unit', [ - 'clean:server', - 'concurrent:test', - 'postcss', - 'connect:test', - 'karma' - // 'coverage' - add back if we want to enforce coverage percentages - ]); - - // test as an alias to unit. after updating protractor, - // will make test an alias for both unit & e2e - grunt.registerTask('test', ['test-unit']); + grunt.registerTask('test-unit', + grunt.option('skipRebuild') ? + [ + 'connect:test', + 'karma' + ] : + [ + 'clean:server', + 'concurrent:test', + 'postcss', + 'connect:test', + 'karma' + // 'coverage' - add back if we want to enforce coverage percentages + ] + ); + // ex: + // grunt test-integration + // grunt test-integration --baseUrl= (for build environments) + // grunt test-integration --skipRebuild (to save time between test runs) + // grunt test-integration --suite=login,create-project --skipRebuild (to run specific suites) grunt.registerTask('test-integration', - // if a baseUrl is defined assume we dont want to run the local grunt server - grunt.option('baseUrl') ? - [isMac ? 'protractor:mac' : 'protractor:default'] : + ( grunt.option('baseUrl') || grunt.option('skipRebuild') ) ? + [ + 'connect:test', + 'protractor:default' + ] : [ 'clean:server', 'development-build', 'postcss', 'connect:test', 'add-redirect-uri', - (isMac ? 'protractor:mac' : 'protractor:default'), + 'protractor:default', 'clean:server' ] - ); + ); + + // alias + grunt.registerTask('test-e2e', ['test-integration']); + + grunt.registerTask('test', ['test-unit', 'test-integration']); grunt.registerTask('build', [ 'clean:dist', diff --git a/Makefile b/Makefile index dfea750c40..df4ceec200 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,6 @@ build: install # make test GRUNT_FLAGS='--gruntfile=~/special/Gruntfile.js' test: build hack/verify-dist.sh - hack/test-headless.sh test $(GRUNT_FLAGS) + hack/test-headless.sh test-unit $(GRUNT_FLAGS) hack/test-headless.sh test-integration $(GRUNT_FLAGS) .PHONY: test diff --git a/package.json b/package.json index 506510ab4d..d3f4202652 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "grunt-newer": "0.7.0", "grunt-ng-annotate": "0.3.2", "grunt-postcss": "^0.8.0", - "grunt-protractor-runner": "1.2.1", + "grunt-protractor-runner": "^5.0.0", "grunt-replace": "1.0.1", "grunt-svgmin": "4.0.0", "grunt-usemin": "2.4.0", @@ -42,7 +42,8 @@ "imagemin": "1.0.5", "jasmine-beforeall": "0.1.1", "jasmine-core": "^2.8.0", - "jasmine-spec-reporter": "1.1.2", + "jasmine-reporters": "^2.2.1", + "jasmine-spec-reporter": "4.2.1", "jshint-stylish": "0.2.0", "karma": "^1.7.1", "karma-chrome-launcher": "^2.2.0", @@ -57,7 +58,8 @@ "less": "2.6.1", "load-grunt-tasks": "0.4.0", "lodash": "3.10.1", - "protractor": "1.7.0", + "protractor": "^5.1.1", + "protractor-jasmine2-screenshot-reporter": "^0.4.1", "protractor-screenshot-reporter": "^0.0.5", "serve-static": "1.10.2", "time-grunt": "0.3.2" @@ -73,6 +75,7 @@ "serve": "grunt serve", "start": "grunt serve", "test-integration": "grunt test-integration", - "postinstall": "node test/upgrade-selenium.js && node_modules/protractor/bin/webdriver-manager update" + "postinstall": "node_modules/protractor/bin/webdriver-manager update", + "webdriver-status": "node_modules/protractor/bin/webdriver-manager status" } } diff --git a/test/.jshintrc b/test/.jshintrc index 503db07b9f..5b2be1f648 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -26,6 +26,7 @@ "afterAll": false, "angular": false, "by": false, + "By": false, "before": false, "beforeAll": false, "beforeEach": false, diff --git a/test/integration/e2e.js b/test/integration/e2e.js deleted file mode 100644 index 6d76ce6e30..0000000000 --- a/test/integration/e2e.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict'; - -require('jasmine-beforeall'); -var h = require('./helpers.js'); - -describe('', function() { - afterAll(function(){ - h.afterAllTeardown(); - }); - - // This UI test suite expects to be run as part of hack/test-end-to-end.sh - // It requires the example project be created with all of its resources in order to pass - - describe('unauthenticated user', function() { - beforeEach(function() { - h.commonSetup(); - }); - - afterEach(function() { - h.commonTeardown(); - }); - - it('should be able to log in', function() { - browser.get('/'); - // The login page doesn't use angular, so we have to use the underlying WebDriver instance - var driver = browser.driver; - driver.wait(function() { - return driver.isElementPresent(by.name("username")); - }, 3000); - - expect(browser.driver.getCurrentUrl()).toMatch(/\/login/); - expect(browser.driver.getTitle()).toMatch(/Login -/); - - h.login(true); - - expect(browser.getTitle()).toEqual("OpenShift Web Console"); - expect(element(by.css(".navbar-iconic .username")).getText()).toEqual("e2e-user"); - }); - - }); - - describe('authenticated e2e-user', function() { - beforeEach(function() { - h.commonSetup(); - h.login(); - }); - - afterEach(function() { - h.commonTeardown(); - }); - - describe('with test project', function() { - it('should be able to list the test project', function() { - browser.get('/').then(function() { - h.waitForPresence('h2.project', 'test'); - }); - }); - - it('should have access to the test project', function() { - h.goToPage('/project/test'); - h.waitForPresence('.navbar-project-menu .filter-option', 'test'); - h.waitForPresence('h1', 'Overview'); - h.waitForPresence('.component .service', 'database'); - h.waitForPresence('.component .service', 'frontend'); - h.waitForPresence('.component .route', 'www.example.com'); - h.waitForPresence('.pod-template-build a', '#1'); - h.waitForPresence('.deployment-trigger', 'from image change'); - - // Check the pod count inside the donut chart for each rc. - h.waitForPresence('#service-database .donut-title-big-pf', '1'); - h.waitForPresence('#service-frontend .donut-title-big-pf', '2'); - - // TODO: validate correlated images, builds, source - }); - }); - }); -}); diff --git a/test/integration/environment.js b/test/integration/environment.js new file mode 100644 index 0000000000..4e9f423bfb --- /dev/null +++ b/test/integration/environment.js @@ -0,0 +1,4 @@ +module.exports = { + baseUrl: 'https://localhost:9000/dev-console/', + loginUrl: 'https://127.0.0.1:8443/login' +}; diff --git a/test/integration/features/user_adds_imagestream_to_project.spec.js b/test/integration/features/user_adds_imagestream_to_project.spec.js index bf3f905be4..9d9581ead0 100644 --- a/test/integration/features/user_adds_imagestream_to_project.spec.js +++ b/test/integration/features/user_adds_imagestream_to_project.spec.js @@ -1,22 +1,23 @@ 'use strict'; -const h = require('../helpers'); +const common = require('../helpers/common'); +const matchers = require('../helpers/matchers'); const projectHelpers = require('../helpers/project'); -const CatalogPage = require('../page-objects/catalog').CatalogPage; + +const CatalogPage = require('../page-objects/legacyCatalog').LegacyCatalogPage; const CreateProjectPage = require('../page-objects/createProject').CreateProjectPage; const ImageStreamsPage = require('../page-objects/imageStreams').ImageStreamsPage; + const centosImageStream = require('../fixtures/image-streams-centos7.json'); describe('User adds an image stream to a project', () => { beforeEach(() => { - h.commonSetup(); - h.login(); - projectHelpers.deleteAllProjects(); + common.beforeEach(); }); afterEach(() => { - h.commonTeardown(); + common.afterEach(); }); describe('after creating a new project', () => { @@ -31,10 +32,11 @@ describe('User adds an image stream to a project', () => { catalogPage .processImageStream(JSON.stringify(centosImageStream)) .then(() => { - // verify we have the nodejs image stream loaded let imageStreamsPage = new ImageStreamsPage(project); imageStreamsPage.visit(); - expect(element(by.cssContainingText('td', 'nodejs')).isPresent()).toBe(true); // TODO: use fixture + // TODO: this is not a good test. The output logs will just say + // expected false to be true. Tests should be much more explicit. + matchers.expectElementToBeVisible(element(by.cssContainingText('td', 'nodejs'))); }); }); }); diff --git a/test/integration/features/user_adds_template_to_project.spec.js b/test/integration/features/user_adds_template_to_project.spec.js index e8c19a30f4..14f8fea07f 100644 --- a/test/integration/features/user_adds_template_to_project.spec.js +++ b/test/integration/features/user_adds_template_to_project.spec.js @@ -1,26 +1,31 @@ 'use strict'; -const h = require('../helpers'); +const common = require('../helpers/common'); +const matchers = require('../helpers/matchers'); const projectHelpers = require('../helpers/project'); -const CatalogPage = require('../page-objects/catalog').CatalogPage; + +const CatalogPage = require('../page-objects/legacyCatalog').LegacyCatalogPage; const CreateProjectPage = require('../page-objects/createProject').CreateProjectPage; const DeploymentsPage = require('../page-objects/deployments').DeploymentsPage; const ServicesPage = require('../page-objects/services').ServicesPage; const RoutesPage = require('../page-objects/routes').RoutesPage; + const nodeMongoTemplate = require('../fixtures/nodejs-mongodb'); describe('User adds a template to a project', () => { beforeEach(() => { - h.commonSetup(); - h.login(); - projectHelpers.deleteAllProjects(); + common.beforeEach(); }); afterEach(() => { - h.commonTeardown(); + common.afterEach(); }); + // TODO: the expect() statements below are using hard-coded values + // rather than testing against the fixture itself. This is fine for + // now, but if we ever update the fixture the tests will likely break. + // In addition, expect(element).toBeTruthy() is not meaningful. describe('after creating a new project', () => { describe('using the Import YAML tab', () => { it('should process and create the objects in the template', () => { @@ -33,28 +38,29 @@ describe('User adds a template to a project', () => { catalogPage .processTemplate(JSON.stringify(nodeMongoTemplate)) .then((createFromTemplatePage) => { - createFromTemplatePage.clickCreate(); // implicit redirect to overview page + // implicit redirect to overview page + createFromTemplatePage.clickCreate(); + // verify we have the 2 deployments in the template let deploymentsPage = new DeploymentsPage(project); deploymentsPage.visit(); - expect(element(by.cssContainingText('td', 'mongodb')).isPresent()).toBe(true); // TODO: use fixture - expect(element(by.cssContainingText('td', 'nodejs-mongodb-example')).isPresent()).toBe(true); // TODO: use fixture + matchers.expectElementToBeVisible(element(by.cssContainingText('td a', 'mongodb'))); + matchers.expectElementToBeVisible(element(by.cssContainingText('td a', 'nodejs-mongodb-example'))); + // verify we have the two services in the template let servicesPage = new ServicesPage(project); servicesPage.visit(); - expect(element(by.cssContainingText('td', 'mongodb')).isPresent()).toBe(true); // TODO: use fixture - expect(element(by.cssContainingText('td', 'nodejs-mongodb-example')).isPresent()).toBe(true); // TODO: use fixture + matchers.expectElementToBeVisible(element(by.cssContainingText('td a', 'mongodb'))); + matchers.expectElementToBeVisible(element(by.cssContainingText('td a', 'nodejs-mongodb-example'))); + // verify we have one route for the mongo app let routesPage = new RoutesPage(project); routesPage.visit(); - expect(element(by.cssContainingText('td', 'nodejs-mongodb-example')).isPresent()).toBe(true); // TODO: use fixture + matchers.expectElementToBeVisible(element(by.cssContainingText('td a', 'nodejs-mongodb-example'))); }); }); it('should save the template in the project catalog', () => { - // TODO: same flow as the above test, but use: - // catalogPage.saveTemplate(tpl) - // & assert that the template was added to the catalog in this project let project = projectHelpers.projectDetails(); let createProjectPage = new CreateProjectPage(project); createProjectPage.visit(); @@ -64,11 +70,9 @@ describe('User adds a template to a project', () => { catalogPage .saveTemplate(JSON.stringify(nodeMongoTemplate)) .then(() => { - // once the template processes, we just have to return - // to the catalog and verify the tile exists catalogPage.visit(); - catalogPage.clickCategory('JavaScript'); // TODO: pass in the tile name from the template fixture - catalogPage.findTileBy('Node.js + MongoDB (Ephemeral)', project.name); // TODO: pass in... + catalogPage.clickCategory('JavaScript'); + catalogPage.findTileBy('Node.js + MongoDB (Ephemeral)', project.name); expect(element).toBeTruthy(); }); }); diff --git a/test/integration/features/user_creates_from_url.spec.js b/test/integration/features/user_creates_from_url.spec.js index 8a6a74771b..eb029ab39d 100644 --- a/test/integration/features/user_creates_from_url.spec.js +++ b/test/integration/features/user_creates_from_url.spec.js @@ -1,19 +1,21 @@ 'use strict'; -require('jasmine-beforeall'); - -const h = require('../helpers'); +const common = require('../helpers/common'); const addExtension = require('../helpers/extensions').addExtension; const resetExtensions = require('../helpers/extensions').resetExtensions; const matchersHelpers = require('../helpers/matchers'); const projectHelpers = require('../helpers/project'); const inputsHelpers = require('../helpers/inputs'); +const forms = require('../helpers/forms'); + const CreateFromURLPage = require('../page-objects/createFromURL').CreateFromURLPage; const CreateProjectPage = require('../page-objects/createProject').CreateProjectPage; -const CatalogPage = require('../page-objects/catalog').CatalogPage; +const LegacyCatalogPage = require('../page-objects/legacyCatalog').LegacyCatalogPage; + const nodeMongoTemplate = require('../fixtures/nodejs-mongodb'); const centosImageStream = require('../fixtures/image-streams-centos7.json'); + describe('authenticated e2e-user', function() { let project = projectHelpers.projectDetails(); @@ -31,7 +33,7 @@ describe('authenticated e2e-user', function() { let createProjectPage = new CreateProjectPage(fixturesProject); createProjectPage.visit(); createProjectPage.createProject(); - let catalogPage = new CatalogPage(fixturesProject); + let catalogPage = new LegacyCatalogPage(fixturesProject); // - add an image stream to that namespace catalogPage.visit(); catalogPage.processImageStream(JSON.stringify(centosImageStream)); @@ -40,17 +42,16 @@ describe('authenticated e2e-user', function() { catalogPage.saveTemplate(JSON.stringify(nodeMongoTemplate)); }; - beforeAll(function() { - h.commonSetup(); - h.login(); - projectHelpers.deleteAllProjects(); + // NOTE: beforeAll vs beforeEach. + // these tests only do the setup once. + beforeAll(() => { + common.beforeEach(); setupEnv(); }); - afterAll(function() { - projectHelpers.deleteAllProjects(); + afterAll(() => { + common.afterEach(); resetExtensions(); - h.afterAllTeardown(); }); describe('create from URL', function() { @@ -67,6 +68,7 @@ describe('authenticated e2e-user', function() { let words = project.name.split(' '); let timestamp = words[words.length - 1]; + it('should display details about the the image', function() { let createFromURLPage = new CreateFromURLPage(); createFromURLPage.visit(qs); @@ -77,18 +79,26 @@ describe('authenticated e2e-user', function() { let createFromURLPage = new CreateFromURLPage(); createFromURLPage.visit(qs); createFromURLPage.clickCreateNewProjectTab(); - projectHelpers.createProject(project, 'project/' + project['name'] + 'create/fromimage' + qs); + + let createProjectPage = new CreateProjectPage(project); + createProjectPage.visit(); + createProjectPage.createProject(); + matchersHelpers.expectHeading(heading); + projectHelpers.deleteProject(project); }); - it('should load the image stream in to an existing project and verify the query string params are loaded in to the corresponding form fields', function(){ + it('should load the image stream in to an existing project and verify the query string params are loaded in to the corresponding form fields', function() { + let createProjectPage = new CreateProjectPage(project); + createProjectPage.visit(); + createProjectPage.createProject(); + let createFromURLPage = new CreateFromURLPage(); - projectHelpers.visitCreatePage(); - projectHelpers.createProject(project); createFromURLPage.visit(qs); createFromURLPage.selectExistingProject(timestamp, uri); matchersHelpers.expectHeading(heading); + let nameInput = element(by.model('name')); expect(nameInput.getAttribute('value')).toEqual(name); let sourceURIInput = element(by.model('buildConfig.sourceUrl')); @@ -98,6 +108,7 @@ describe('authenticated e2e-user', function() { expect(sourceRefInput.getAttribute('value')).toEqual(sourceRef); let contextDirInput = element(by.model('buildConfig.contextDir')); expect(contextDirInput.getAttribute('value')).toEqual(contextDir); + projectHelpers.deleteProject(project); }); @@ -112,25 +123,30 @@ describe('authenticated e2e-user', function() { let words = project.name.split(' '); let timestamp = words[words.length - 1]; + it('should display details about the template', function() { let createFromURLPage = new CreateFromURLPage(); createFromURLPage.visit(qs); matchersHelpers.expectHeading(heading); }); + it('should load the template in to a newly created project', function() { let createFromURLPage = new CreateFromURLPage(); createFromURLPage.visit(qs); createFromURLPage.clickCreateNewProjectTab(); - projectHelpers.createProject(project, 'project/' + project['name'] + 'create/fromtemplate' + qs); + forms.submitNewProjectForm(project); matchersHelpers.expectHeading(heading); projectHelpers.deleteProject(project); }); + it('should load the template in an existing project and verify the query string param sourceURL is loaded in to a corresponding form field', function(){ + let createProjectPage = new CreateProjectPage(project); + createProjectPage.visit(); + createProjectPage.createProject(); + let createFromURLPage = new CreateFromURLPage(); - projectHelpers.visitCreatePage(); - projectHelpers.createProject(project); createFromURLPage.visit(qs); createFromURLPage.selectExistingProject(timestamp, uri); matchersHelpers.expectHeading(heading); @@ -140,7 +156,7 @@ describe('authenticated e2e-user', function() { expect(found).toEqual(sourceURL); projectHelpers.deleteProject(project); }); - }); + }); }); @@ -183,6 +199,7 @@ describe('authenticated e2e-user', function() { matchersHelpers.expectAlert('An image stream or template is required.'); }); }); + describe('using both an image stream and a template', function() { it('should display an error about combining resources', function() { let createFromURLPage = new CreateFromURLPage(); @@ -190,6 +207,7 @@ describe('authenticated e2e-user', function() { matchersHelpers.expectAlert('Image streams and templates cannot be combined.'); }); }); + describe('using an invalid app name as a query string param', function() { it('should display an error about the app name', function() { let createFromURLPage = new CreateFromURLPage(); diff --git a/test/integration/features/user_creates_project.spec.js b/test/integration/features/user_creates_project.spec.js index b1d77498bd..1f223d6938 100644 --- a/test/integration/features/user_creates_project.spec.js +++ b/test/integration/features/user_creates_project.spec.js @@ -1,226 +1,96 @@ 'use strict'; -/* jshint unused:false */ - -const h = require('../helpers.js'); -const projectHelpers = require('../helpers/project.js'); - -let goToAddToProjectPage = (projectName) => { - let uri = 'project/' + projectName + '/create'; - h.goToPage(uri); - expect(element(by.cssContainingText('.middle h1', "Create Using Your Code")).isPresent()).toBe(true); - expect(element(by.cssContainingText('.middle h1', "Create Using a Template")).isPresent()).toBe(true); - expect(element(by.model('from_source_url')).isPresent()).toBe(true); - expect(element(by.cssContainingText('.catalog h3 > a', "ruby-helloworld-sample")).isPresent()).toBe(true); -}; - -let goToCreateProjectPage = () => { - h.goToPage('create-project'); - expect(element(by.cssContainingText('.middle h1', "Create Project")).isPresent()).toBe(true); - expect(element(by.model('name')).isPresent()).toBe(true); - expect(element(by.model('displayName')).isPresent()).toBe(true); - expect(element(by.model('description')).isPresent()).toBe(true); -}; - -let requestCreateFromSource = (projectName, sourceUrl) => { - let uri = 'project/' + projectName + '/create'; - h.waitForUri(uri); - h.setInputValue('from_source_url', sourceUrl); - let nextButton = element(by.buttonText('Next')); - browser.wait(protractor.ExpectedConditions.elementToBeClickable(nextButton), 2000); - nextButton.click(); -}; - -let requestCreateFromTemplate = (projectName, templateName) => { - let uri = 'project/' + projectName + '/create'; - h.waitForUri(uri); - let template = element(by.cssContainingText('.catalog h3 > a', templateName)); - expect(template.isPresent()).toBe(true); - template.click(); -}; - -let attachBuilderImageToSource = (projectName, builderImageName) => { - let uri = 'project/' + projectName + '/catalog/images'; - h.waitForUri(uri); - expect(element(by.cssContainingText('.middle h1', "Select a builder image")).isPresent()).toBe(true); - let builderImageLink = element(by.cssContainingText('h3 > a', builderImageName)); - expect(builderImageLink.isPresent()).toBe(true); - builderImageLink.click(); -}; - -let createFromSource = (projectName, builderImageName, appName) => { - let uri = 'project/' + projectName + '/create/fromimage'; - h.waitForUri(uri); - expect(element(by.css('.middle .osc-form h1')).getText()).toEqual(builderImageName); - expect(element(by.cssContainingText('h2', "Name")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Routing")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Deployment Configuration")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Build Configuration")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Scaling")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Labels")).isPresent()).toBe(true); - let appNameInput = element(by.name('appname')); - appNameInput.clear(); - appNameInput.sendKeys(appName); - h.clickAndGo('Create', 'project/' + projectName + '/overview'); -}; - -let createFromTemplate = (projectName, templateName, parameterNames, labelNames) => { - let uri = 'project/' + projectName + '/create/fromtemplate'; - h.waitForUri(uri); - expect(element(by.css('.middle .osc-form h1')).getText()).toEqual(templateName); - expect(element(by.cssContainingText('h2', "Images")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Parameters")).isPresent()).toBe(true); - expect(element(by.cssContainingText('h2', "Labels")).isPresent()).toBe(true); - if (parameterNames) { - parameterNames.forEach((val) => { - expect(element(by.cssContainingText('.env-letiable-list label.key', val)).isPresent()).toBe(true); - }); - } - if (labelNames) { - labelNames.forEach((val) => { - expect(element(by.cssContainingText('.label-list span.key', val)).isPresent()).toBe(true); - }); - } - h.clickAndGo('Create', 'project/' + projectName + '/overview'); -}; - -let checkServiceCreated = (projectName, serviceName) => { - h.goToPage('project/' + projectName + '/overview'); - h.waitForPresence('.component .service', serviceName, 10000); - h.goToPage('project/' + projectName + '/browse/services'); - h.waitForPresence('h3', serviceName, 10000); -}; - -let checkProjectSettings = (projectName, displayName, description) => { - let uri = 'project/' + projectName + '/edit'; - h.goToPage(uri); - expect(element(by.css('.middle h1')).getText()).toEqual("Edit Project " + projectName); - expect(element(by.css('#displayName')).getAttribute('value')).toEqual(displayName); - expect(element(by.css('#description')).getAttribute('value')).toEqual(description); -}; - - -describe('', () => { - - afterAll(function(){ - h.afterAllTeardown(); - }); - describe('authenticated e2e-user', () => { +const common = require('../helpers/common'); +const timing = require('../helpers/timing'); - beforeEach(() => { - h.commonSetup(); - h.login(); - }); +const projectHelpers = require('../helpers/project'); +const matchers = require('../helpers/matchers'); - afterEach(() => { - h.commonTeardown(); - }); +const CreateProjectPage = require('../page-objects/createProject').CreateProjectPage; +const OverviewPage = require('../page-objects/overview').OverviewPage; + +const menus = require('../page-objects/menus').menus; + +describe('Authenticated user creates a new project', () => { + + beforeEach(() => { + common.beforeEach(); + }); + + afterEach(() => { + common.afterEach(); + }); + + it('should be able to create a new project', () => { + + let project = projectHelpers.projectDetails(); + let createProjectPage = new CreateProjectPage(project); + createProjectPage.visit(); + createProjectPage.createProject().then((projectList) => { + // sometimes this flakes out. Perhaps the server takes a little too long + // to create the project? + browser.sleep(timing.initialVisit); + // show the project in the list + matchers.expectElementToBeVisible(projectList.findTileBy(project.displayName)); + + // wait a bit to ensure project is created + browser.sleep(timing.initialVisit); + projectList.clickTileBy(project.displayName); + + // click through Application menu + menus.leftNav.clickDeployments(); + matchers.expectHeadingContainsText('Deployments'); + menus.leftNav.clickStatefulSets(); + matchers.expectHeadingContainsText('Stateful Sets'); + menus.leftNav.clickPods(); + matchers.expectHeadingContainsText('Pods'); + menus.leftNav.clickServices(); + matchers.expectHeadingContainsText('Services'); + menus.leftNav.clickRoutes(); + matchers.expectHeadingContainsText('Routes'); + + // click through Builds menu + menus.leftNav.clickBuilds(); + matchers.expectHeadingContainsText('Builds'); + menus.leftNav.clickPipelines(); + matchers.expectHeadingContainsText('Pipelines'); + menus.leftNav.clickImages(); + matchers.expectHeadingContainsText('Image Streams'); + + // click through Resources menu + menus.leftNav.clickQuota(); + matchers.expectHeadingContainsText('Quota'); + menus.leftNav.clickMembership(); + matchers.expectHeadingContainsText('Membership'); + menus.leftNav.clickConfigMaps(); + matchers.expectHeadingContainsText('Config Maps'); + menus.leftNav.clickSecrets(); + matchers.expectHeadingContainsText('Secrets'); + menus.leftNav.clickOtherResources(); + matchers.expectHeadingContainsText('Other Resources'); + + // click remaining primary sidebar items. + menus.leftNav.clickOverview(); + let overviewPage = new OverviewPage(project); + // overview doesn't show a heading, best check is url + matchers.expectPageUrl(overviewPage.getUrl()); + menus.leftNav.clickStorage(); + matchers.expectHeadingContainsText('Storage'); + menus.leftNav.clickMonitoring(); + matchers.expectHeadingContainsText('Monitoring'); + + + // click through some top level menu items + menus.topNav.clickCLI(); + matchers.expectHeadingContainsText('Command Line Tools'); + // we lose our nav on these pages. + browser.navigate().back(); + browser.sleep(timing.navToPage); + menus.topNav.clickAbout(); + matchers.expectHeadingContainsText('Red Hat Openshift'); + // Documentation link leaves console + // Copy Login should also just have its own test - describe('new project', () => { - - describe('when creating a new project', () => { - - it('should be able to show the create project page', goToCreateProjectPage); - - let project = projectHelpers.projectDetails(); - - it('should successfully create a new project', () => { - h.goToPage('projects'); - goToCreateProjectPage(); - projectHelpers.createProject(project, 'projects'); - h.goToPage('project/' + project['name'] + '/overview'); - h.waitForPresence('.project-bar option[selected][value="' + project['name'] + '"]'); - checkProjectSettings(project['name'], project['displayName'], project['description']); - }); - - it('should browse builds', () => { - h.goToPage('project/' + project['name'] + '/browse/builds'); - h.waitForPresence('.middle h1', 'Builds'); - // TODO: validate presented strategies, images, repos - }); - - it('should browse deployments', () => { - h.goToPage('project/' + project['name'] + '/browse/deployments'); - h.waitForPresence(".middle h1", "Deployments"); - // TODO: validate presented deployments - }); - - it('should browse events', () => { - h.goToPage('project/' + project['name'] + '/browse/events'); - h.waitForPresence(".middle h1", "Events"); - // TODO: validate presented events - }); - - it('should browse image streams', () => { - h.goToPage('project/' + project['name'] + '/browse/images'); - h.waitForPresence(".middle h1", "Image Streams"); - // TODO: validate presented images - }); - - it('should browse pods', () => { - h.goToPage('project/' + project['name'] + '/browse/pods'); - h.waitForPresence(".middle h1", "Pods"); - // TODO: validate presented pods, containers, correlated images, builds, source - }); - - it('should browse services', () => { - h.goToPage('project/' + project['name'] + '/browse/services'); - h.waitForPresence(".middle h1", "Services"); - // TODO: validate presented ports, routes, selectors - }); - - it('should validate taken name when trying to create', () => { - goToCreateProjectPage(); - element(by.model('name')).clear().sendKeys(project['name']); - element(by.buttonText("Create")).click(); - expect(element(by.css("[ng-if=nameTaken]")).isDisplayed()).toBe(true); - expect(browser.getCurrentUrl()).toMatch(/\/create-project$/); - }); - - it('should delete a project', () => { - projectHelpers.deleteProject(project); - }); - - /* - describe('when using console-integration-test-project', function() { - describe('when adding to project', function() { - it('should view the create page', function() { goToAddToProjectPage("console-integration-test-project"); }); - - it('should create from source', function() { - let projectName = "console-integration-test-project"; - let sourceUrl = "https://github.com/openshift/rails-ex#master"; - let appName = "rails-ex-mine"; - let builderImage = "ruby"; - - goToAddToProjectPage(projectName); - requestCreateFromSource(projectName, sourceUrl); - attachBuilderImageToSource(projectName, builderImage); - createFromSource(projectName, builderImage, appName); - checkServiceCreated(projectName, appName); - }); - - it('should create from template', function() { - let projectName = "console-integration-test-project"; - let templateName = "ruby-helloworld-sample"; - let parameterNames = [ - "ADMIN_USERNAME", - "ADMIN_PASSWORD", - "MYSQL_USER", - "MYSQL_PASSWORD", - "MYSQL_DATABASE" - ]; - let labelNames = ["template"]; - - goToAddToProjectPage(projectName); - requestCreateFromTemplate(projectName, templateName); - createFromTemplate(projectName, templateName, parameterNames, labelNames); - checkServiceCreated(projectName, "frontend"); - checkServiceCreated(projectName, "database"); - }); - }); - }); - */ - }); }); }); }); diff --git a/test/integration/features/user_logs_in.spec.js b/test/integration/features/user_logs_in.spec.js new file mode 100644 index 0000000000..c2cbfcf2cc --- /dev/null +++ b/test/integration/features/user_logs_in.spec.js @@ -0,0 +1,25 @@ +'use strict'; + +const matchersHelpers = require('../helpers/matchers'); +const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); +const LoginPage = require('../page-objects/login').LoginPage; +const menus = require('../page-objects/menus').menus; + + +describe('unauthenticated user', () => { + + describe('attempts to login to the web console', () => { + it('should login the user and then be redirected to the catalog', () => { + logger.log('login'); + let loginPage = new LoginPage(); + loginPage.login(); + browser.driver.sleep(timing.initialVisit); + matchersHelpers.expectHeading('Browse Catalog'); + + browser.driver.sleep(timing.initialVisit); + logger.log('logout'); + menus.topNav.clickLogout(); + }); + }); +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 15b814b6c0..76319bb463 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -18,25 +18,6 @@ exports.afterAllTeardown = function() { browser.driver.sleep(1000); }; -exports.login = function(loginPageAlreadyLoaded) { - // The login page doesn't use angular, so we have to use the underlying WebDriver instance - var driver = browser.driver; - if (!loginPageAlreadyLoaded) { - browser.get('/'); - driver.wait(function() { - return driver.isElementPresent(by.name("username")); - }, 3000); - } - - driver.findElement(by.name("username")).sendKeys("e2e-user"); - driver.findElement(by.name("password")).sendKeys("e2e-user"); - driver.findElement(by.css("button[type='submit']")).click(); - - driver.wait(function() { - return driver.isElementPresent(by.css(".navbar-iconic .username")); - }, 5000); -}; - exports.clickAndGo = function(buttonText, uri) { var button = element(by.buttonText(buttonText)); browser.wait(EC.elementToBeClickable(button), 2000); diff --git a/test/integration/helpers/common.js b/test/integration/helpers/common.js new file mode 100644 index 0000000000..782ba5a099 --- /dev/null +++ b/test/integration/helpers/common.js @@ -0,0 +1,31 @@ +'use strict'; + +const windowHelper = require('../helpers/window'); +const projectHelpers = require('../helpers/project'); +const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); +const menus = require('../page-objects/menus').menus; +const LoginPage = require('../page-objects/login').LoginPage; + +exports.beforeAll = () => { + windowHelper.setSize(); +}; + +exports.beforeEach = () => { + logger.log('login'); + // we manually bootstrap angular, so it is suggested to do this + // call up front, however it has not been needed thus far. + // browser.waitForAngular(); + let loginPage = new LoginPage(); + loginPage.login(); + browser.driver.sleep(timing.standardDelay); + projectHelpers.deleteAllProjects(); +}; + + +exports.afterEach = () => { + menus.topNav.clickLogout(); + browser.sleep(timing.standardDelay); + logger.log('logout'); + windowHelper.clearStorage(); +}; diff --git a/test/integration/helpers/forms.js b/test/integration/helpers/forms.js new file mode 100644 index 0000000000..a3c2f4a222 --- /dev/null +++ b/test/integration/helpers/forms.js @@ -0,0 +1,23 @@ +'use strict'; + +const timing = require('./timing'); +const setInputValue = require('./inputs').setInputValue; + +exports.submitNewProjectForm = function(project) { + + if(project.name) { + setInputValue(by.model('name'), project.name); + } + if(project.displayName) { + setInputValue(by.model('displayName'), project.displayName); + } + if(project.description) { + setInputValue(by.model('description'), project.description); + } + return element(by.buttonText('Create')).click().then(() => { + // There is an implicit redirect in these forms, but it + // is not always to the same destination. Each test will + // have to be responsible for handling its own flow. + return browser.sleep(timing.implicitRedirect); + }); +}; diff --git a/test/integration/helpers/hasAngularDefined.js b/test/integration/helpers/hasAngularDefined.js new file mode 100644 index 0000000000..645b849304 --- /dev/null +++ b/test/integration/helpers/hasAngularDefined.js @@ -0,0 +1,22 @@ +'use strict'; + + + +// usage: +// somePage.visit(); +// waitForRedirect(somePage.getUrl()); +// expect(somethingAfterRedirect).toBe(thisOrThat); +exports.waitForRedirect = function(urlFragment, timeout) { + let hasRedirected = false; + browser.wait(() => { + browser + .getCurrentUrl() + .then((url) => { + return url.includes(urlFragment); + }) + .then((hasNavigated) => { + hasRedirected = hasNavigated; + }); + return hasRedirected; + }, timeout || 5000); +}; diff --git a/test/integration/helpers/inputs.js b/test/integration/helpers/inputs.js index 0477b764ac..0c6f0aeb28 100644 --- a/test/integration/helpers/inputs.js +++ b/test/integration/helpers/inputs.js @@ -1,5 +1,13 @@ 'use strict'; -/*jshint esversion: 6 */ + + +const setInputValue = (selector, value) => { + var input = element(selector); + input.clear(); + input.sendKeys(value); + expect(input.getAttribute("value")).toBe(value); + return input; +}; // is a pain to get an array of input values because .getAttribute() // is a promise @@ -56,6 +64,7 @@ const uncheck = (checkboxElem) => { }); }; +exports.setInputValue = setInputValue; exports.getInputValues = getInputValues; exports.findValueInInputs = findValueInInputs; diff --git a/test/integration/helpers/matchers.js b/test/integration/helpers/matchers.js index 09f69999a5..9bee4b4e4f 100644 --- a/test/integration/helpers/matchers.js +++ b/test/integration/helpers/matchers.js @@ -1,16 +1,49 @@ 'use strict'; +let wait = require('./wait'); + exports.expectAlert = (msg) => { - expect(element(by.css('.alert')).getText()).toEqual('error\n' + msg); + let elem = element(by.css('.alert')); + wait.forElem(elem); + expect(elem.getText()).toEqual('error\n' + msg); }; exports.expectHeading = (text, level) => { - expect(element(by.css(level || '.middle h1')).getText()).toEqual(text); + let elem = element.all(by.css(level || '.middle h1')); + wait.forElem(elem); + expect(elem.first().getText()).toEqual(text); }; -exports.expectPartialHeading = (partialText, level, caseSensitive) => { - element(by.css(level || '.middle h1')).getText().then((text) => { +// on some pages we hide help links within the heading, +// this lets the assert be a little more fuzzy and ignore extra markup +exports.expectHeadingContainsText = (partialText, caseSensitive, level) => { + let elem = element.all(by.css(level || '.middle h1')); + wait.forElem(elem); + elem.first().getText().then((text) => { + let toMatch = caseSensitive ? partialText : partialText.toLowerCase(); text = caseSensitive ? text : text.toLowerCase(); - expect(text).toContain(partialText); + expect(text).toContain(toMatch); + }); +}; + +exports.expectElementToExist = (elem) => { + expect(elem.isPresent()).toBe(true); +}; + +exports.expectElementToBeVisible = (elem) => { + expect(elem.isDisplayed()).toBeTruthy(); +}; + +exports.expectElementToBeHidden = (elem) => { + expect(elem.isDisplayed()).toBeFalsy(); +}; + +exports.expectPageUrl = (pageUrl) => { + browser.getCurrentUrl().then((actualUrl) => { + // NOTE: this uses contains instead of equals + // to avoid worrying about query string inconsistencies, etc. + // we can add a flag for exact matching if there comes a + // point when it would be helpful. + expect(actualUrl).toContain(pageUrl); }); }; diff --git a/test/integration/helpers/nonAngular.js b/test/integration/helpers/nonAngular.js new file mode 100644 index 0000000000..8623d01660 --- /dev/null +++ b/test/integration/helpers/nonAngular.js @@ -0,0 +1,18 @@ +'use strict'; + +exports.nonAngular = function(func) { + // kill the sync to prep for a drop into + // raw webdriver calls. the browser sync is + // how protractor knows to wait around for + // angular + browser.ignoreSynchronization = true; + // func should be a wrapper around raw webdriver calls, + // not protractor. Therefore the following: + // - will work: + // - browser.driver.findElement(by.css('.title')).getText(); + // - will not work: + // - element(by.css('.title')).getText(); + func(); + // set the sync back for tests that follow + browser.ignoreSynchronization = false; +}; diff --git a/test/integration/helpers/timing.js b/test/integration/helpers/timing.js new file mode 100644 index 0000000000..0a945c87c8 --- /dev/null +++ b/test/integration/helpers/timing.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + // arbitrary wait, perhaps just for a render + standardDelay: 500, + // time to wait for initial Page.visit(), more bootstrapping, etc + initialVisit: 1000, + // implicit redirects do not cause browser.refresh() + implicitRedirect: 500, + // sufficient for menus.someNav.clickSomething() + navToPage: 500, + // sufficient for a show/hide delay for a UI element. + // protractor may fail if an element is on page but hidden + openMenu: 300, + scroll: 100, + // ex: + // let until = protractor.ExpectedConditions.until; + // browser.wait( + // until.presenceOf(elem), + // timing.waitForElement, + // 'Elem did not appear') + waitForElement: 1000, + maxWaitForElement: 15000 +}; diff --git a/test/integration/helpers/wait.js b/test/integration/helpers/wait.js new file mode 100644 index 0000000000..6859b5e6f0 --- /dev/null +++ b/test/integration/helpers/wait.js @@ -0,0 +1,16 @@ +'use strict'; + +var EC = protractor.ExpectedConditions; +var timing = require('./timing'); + +exports.forElem = (elem) => { + var selector = elem.locator().toString(); + var errorMessage = `Element taking too long to appear in the DOM: ${selector}`; + return browser.wait(EC.presenceOf(elem), timing.maxWaitForElement, errorMessage); +}; + +exports.forClickableElem = (elem) => { + var selector = elem.locator().toString(); + var errorMessage = `Element taking too long to become clickable: ${selector}`; + return browser.wait(EC.elementToBeClickable(elem), timing.maxWaitForElement, errorMessage); +}; diff --git a/test/integration/helpers/waitForRedirect.js b/test/integration/helpers/waitForRedirect.js new file mode 100644 index 0000000000..d18528ac6b --- /dev/null +++ b/test/integration/helpers/waitForRedirect.js @@ -0,0 +1,20 @@ +'use strict'; + +// usage: +// somePage.visit(); +// waitForRedirect(somePage.url); +// expect(somethingAfterRedirect).toBe(thisOrThat); +exports.waitForRedirect = function(urlFragment, timeout) { + let hasRedirected = false; + browser.wait(() => { + browser + .getCurrentUrl() + .then((url) => { + return url.includes(urlFragment); + }) + .then((hasNavigated) => { + hasRedirected = hasNavigated; + }); + return hasRedirected; + }, timeout || 5000); +}; diff --git a/test/integration/helpers/window.js b/test/integration/helpers/window.js new file mode 100644 index 0000000000..d5c59b1241 --- /dev/null +++ b/test/integration/helpers/window.js @@ -0,0 +1,41 @@ +'use strict'; + +const timing = require('./timing'); + +exports.setSize = (height = 2024, width = 2048) => { + browser.driver.manage().window().setSize(width, height); +}; + +exports.maximize = () => { + return browser.driver.manage().window().maximize(); +}; + + +exports.clearStorage = () => { + browser.executeScript('window.sessionStorage.clear();'); + browser.executeScript('window.localStorage.clear();'); +}; + +// ex: +// scroll.toBottom().then(() => { /* do work */ }); +exports.scrollToBottom = () => { + return browser + .executeScript('window.scrollTo(0,document.body.scrollHeight);') + .then(() => { + // brief wait to ensure components come into view before + // running the next selectors + browser.sleep(timing.scroll); + }); +}; + +exports.scrollToTop = () => { + return browser.executeScript('window.scrollTo(0,0);'); +}; + +exports.scrollTo = (elem) => { + return browser.actions().mouseMove(elem); +}; + +exports.scrollToElement = (elem) => { + return browser.actions().mouseMove(elem).perform(); +}; diff --git a/test/integration/page-objects/createFromTemplate.js b/test/integration/page-objects/createFromTemplate.js index 86f098d4d1..b07d1a25cf 100644 --- a/test/integration/page-objects/createFromTemplate.js +++ b/test/integration/page-objects/createFromTemplate.js @@ -1,8 +1,10 @@ 'use strict'; -const h = require('../helpers'); +const winHelper = require('../helpers/window'); +const timing = require('../helpers/timing'); +const wait = require('../helpers/wait'); const Page = require('./page').Page; -const scroller = require('../helpers/scroll'); + class CreateFromTemplatePage extends Page { constructor(project, menu) { @@ -16,12 +18,18 @@ class CreateFromTemplatePage extends Page { return url; } clickCreate() { - scroller.toBottom(); let button = element(by.buttonText('Create')); - h.waitForElem(button); + winHelper.scrollToElement(button); + wait.forClickableElem(button); return button.click().then(() => { - const OverviewPage = require('./overview').OverviewPage; - return new OverviewPage(this.project); + // hiding a delay in here since the action will cause the server + // to create resources & any actions following clickCreate() will + // likely expect new DOM nodes to exist + return browser.sleep(timing.implicitRedirect).then(() => { + // implicit redirect + const NextStepsPage = require('./nextSteps').NextStepsPage; + return new NextStepsPage(this.project); + }); }); } } diff --git a/test/integration/page-objects/createFromURL.js b/test/integration/page-objects/createFromURL.js index a90a7812eb..e56693bafb 100644 --- a/test/integration/page-objects/createFromURL.js +++ b/test/integration/page-objects/createFromURL.js @@ -1,8 +1,10 @@ 'use strict'; -const Page = require('./page').Page; -const logger = require('../helpers/logger'); const h = require('../helpers.js'); +const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); + +const Page = require('./page').Page; class CreateFromURLPage extends Page { constructor(project, menu) { @@ -11,12 +13,15 @@ class CreateFromURLPage extends Page { getUrl(qs) { return 'create' + qs; } - // TODO: for some reason, the Page.visit()'s use of helpers.goToPage() + // FIXME: for some reason, the Page.visit()'s use of helpers.goToPage() // does not work here, so we have to override. visit(qs) { this.qs = qs; - logger.log('visiting url:', this.getUrl(this.qs)); - return browser.get('create' + this.qs); + logger.log('Visiting url (refresh):', this.getUrl(this.qs)); + return browser.get('create' + this.qs).then(() => { + // Calling visit performs a browser refresh each time. + return browser.sleep(timing.initialVisit); + }); } clickCreateNewProjectTab() { return element(by.css('.nav-tabs')).isPresent() diff --git a/test/integration/page-objects/createProject.js b/test/integration/page-objects/createProject.js index aa327a22f7..b29f5b2094 100644 --- a/test/integration/page-objects/createProject.js +++ b/test/integration/page-objects/createProject.js @@ -1,30 +1,25 @@ 'use strict'; -const h = require('../helpers.js'); +const forms = require('../helpers/forms'); +const timing = require('../helpers/timing'); const Page = require('./page').Page; +const ProjectList = require('./projectList').ProjectList; class CreateProjectPage extends Page { constructor(project, menu) { super(project, menu); } + getUrl() { return 'create-project'; } - enterProjectInfo() { - for (let key in this.project) { - h.waitForElem( element( by.model( key ))); - h.setInputValue(key, this.project[key]); - } - return this; - } - submit() { - let button = element(by.buttonText('Create')); - button.click(); - } - // TODO: there is an implicit navigation here, this should return a new Overview page for clarity + createProject() { - this.enterProjectInfo(); - return this.submit(); + return forms.submitNewProjectForm(this.project).then(() => { + return browser.sleep(timing.implicitRedirect).then(() => { + return new ProjectList(); + }); + }); } } diff --git a/test/integration/page-objects/catalog.js b/test/integration/page-objects/legacyCatalog.js similarity index 80% rename from test/integration/page-objects/catalog.js rename to test/integration/page-objects/legacyCatalog.js index 2a7b8cc2a2..0905ecfbca 100644 --- a/test/integration/page-objects/catalog.js +++ b/test/integration/page-objects/legacyCatalog.js @@ -1,15 +1,17 @@ 'use strict'; const h = require('../helpers.js'); +const timing = require('../helpers/timing'); const Page = require('./page').Page; const AddTemplateModal = require('./modals/addTemplateModal').AddTemplateModal; -class CatalogPage extends Page { +class LegacyCatalogPage extends Page { constructor(project, menu) { super(project, menu); } getUrl() { - // TODO: ?tab=tab=fromFile, ?tab=fromCatalog, ?tab=deployImage + // This should probably support tabs: + // ?tab=tab=fromFile, ?tab=fromCatalog, ?tab=deployImage return 'project/' + this.project.name + '/create'; } _findTabs() { @@ -56,12 +58,17 @@ class CatalogPage extends Page { } submitTemplate() { element(by.cssContainingText('.btn-primary','Create')).click(); - return browser.sleep(500).then(() => { + return browser.sleep(timing.implicitRedirect).then(() => { return new AddTemplateModal(this.project); }); } submitImageStream() { - element(by.cssContainingText('.btn-primary','Create')).click(); + return element(by.cssContainingText('.btn-primary','Create')) + .click().then(() => { + // delay to allow the server to generate + // resources before continuing through the flow. + return browser.sleep(timing.standardDelay); + }); } processTemplate(templateStr) { this.clickImport(); @@ -89,4 +96,4 @@ class CatalogPage extends Page { } } -exports.CatalogPage = CatalogPage; +exports.LegacyCatalogPage = LegacyCatalogPage; diff --git a/test/integration/page-objects/login.js b/test/integration/page-objects/login.js new file mode 100644 index 0000000000..1a3c7b4bcf --- /dev/null +++ b/test/integration/page-objects/login.js @@ -0,0 +1,30 @@ +'use strict'; + +const environment = require('../environment'); +const nonAngular = require('../helpers/nonAngular').nonAngular; + +// login page is a non-angular page +class LoginPage { + constructor() { + // don't call super, this is a non-angular page + } + // its just convenient to roll everything up + // in one method for this page. + login() { + browser.driver.sleep(0).then(() => { + browser.driver.get(environment.baseUrl); + browser.driver.sleep(1000).then(() => { + nonAngular(() => { + browser.driver.wait(browser.driver.findElement(by.name('username'))).then(() => { + browser.driver.findElement(by.name('username')).sendKeys('e2e-user'); + browser.driver.findElement(by.name('password')).sendKeys('e2e-user'); + browser.driver.findElement(by.css("button[type='submit']")).click(); + }, 1000); + }); + }); + + }); + } +} + +exports.LoginPage = LoginPage; diff --git a/test/integration/page-objects/menus.js b/test/integration/page-objects/menus.js index 4f765744d2..c674c276f3 100644 --- a/test/integration/page-objects/menus.js +++ b/test/integration/page-objects/menus.js @@ -1,80 +1,145 @@ 'use strict'; -// helpers -var clickNestedMenuItem = function(mainMenuSelector, childMenuSelector) { +const timing = require('../helpers/timing'); + +let selectors = { + topNav: '.dropdown-menu li a', + // making child selectors explicit since we use the same class names for nested menus + sidePrimary: 'sidebar .nav-vertical-primary > .list-group > .list-group-item', + sideSecondary: 'sidebar .nav-pf-secondary-nav .list-group-item' +}; + + + +let clickNestedMenuItem = function(mainMenuSelector, childMenuSelector) { return element(mainMenuSelector).click().then(() => { - return browser.sleep(300).then(() => { - return element(childMenuSelector).click(); + return browser.sleep(timing.openMenu).then(() => { + element(childMenuSelector).getText().then((txt) => { + console.log('Clicking menu item: ', txt); + }); + return element(childMenuSelector).click().then(() => { + return browser.sleep(timing.navToPage); + }); }); }); }; exports.menus = { topNav: { + // NOTE: this links out of the console clickDocumentation: () => { - return clickNestedMenuItem(by.id('help-dropdown'), by.cssContainingText('.dropdown.open', 'Documentation')); + return clickNestedMenuItem( + by.id('help-dropdown'), + by.cssContainingText(selectors.topNav, 'Documentation')); }, + clickTourHomePage: () => { + return clickNestedMenuItem( + by.id('help-dropdown'), + by.cssContainingText(selectors.topNav, 'Tour Home Page')); + }, + // NOTE: navigates to a page w/o menus. clickCLI: () => { - return clickNestedMenuItem(by.id('help-dropdown'), by.css('.dropdown.open', 'Command Line Tools')); + return clickNestedMenuItem( + by.id('help-dropdown'), + by.cssContainingText(selectors.topNav, 'Command Line Tools')); }, clickAbout: () => { - return clickNestedMenuItem(by.id('help-dropdown'), by.css('.dropdown.open', 'About')); + return clickNestedMenuItem( + by.id('help-dropdown'), + by.cssContainingText(selectors.topNav, 'About')); + }, + + clickCopyLogin: () => { + return clickNestedMenuItem( + by.id('user-dropdown'), + by.cssContainingText(selectors.topNav, 'Copy Login Command')); }, clickLogout: () => { - return clickNestedMenuItem(by.id('iser-dropdown'), by.css('.dropdown.open', 'Logout')); + return clickNestedMenuItem( + by.id('user-dropdown'), + by.cssContainingText(selectors.topNav, 'Log Out')); } }, // example: // AnyPage.leftNav.clickPods(); leftNav: { clickOverview: () => { - return element(by.cssContainingText('.dropdown-toggle', 'Overview').row(0)).click(); + return element(by.cssContainingText(selectors.sidePrimary, 'Overview')).click(); }, // applications submenu clickDeployments: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Applications'), by.cssContainingText('a', 'Deployments')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Applications'), + by.cssContainingText(selectors.sideSecondary, 'Deployments')); + }, + clickStatefulSets: () => { + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Applications'), + by.cssContainingText(selectors.sideSecondary, 'Stateful Sets')); }, clickPods: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Applications'), by.cssContainingText('a', 'Pods')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Applications'), + by.cssContainingText(selectors.sideSecondary, 'Pods')); }, clickServices: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Applications'), by.cssContainingText('a', 'Services')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Applications'), + by.cssContainingText(selectors.sideSecondary, 'Services')); }, clickRoutes: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Applications'), by.cssContainingText('a', 'Routes')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Applications'), + by.cssContainingText(selectors.sideSecondary, 'Routes')); }, // builds submenu clickBuilds: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Builds'), by.cssContainingText('a', 'Builds')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Builds'), + by.cssContainingText(selectors.sideSecondary, 'Builds')); }, clickPipelines: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Builds'), by.cssContainingText('a', 'Pipelines')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Builds'), + by.cssContainingText(selectors.sideSecondary, 'Pipelines')); }, clickImages: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Builds'), by.cssContainingText('a', 'Images')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Builds'), + by.cssContainingText(selectors.sideSecondary, 'Images')); }, // resources submenu clickQuota: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Resources'), by.cssContainingText('a', 'Quota')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Resources'), + by.cssContainingText(selectors.sideSecondary, 'Quota')); }, clickMembership: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Resources'), by.cssContainingText('a', 'Membership')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Resources'), + by.cssContainingText(selectors.sideSecondary, 'Membership')); }, clickConfigMaps: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Resources'), by.cssContainingText('a', 'Config Maps')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Resources'), + by.cssContainingText(selectors.sideSecondary, 'Config Maps')); }, clickSecrets: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Resources'), by.cssContainingText('a', 'Secrets')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Resources'), + by.cssContainingText(selectors.sideSecondary, 'Secrets')); }, clickOtherResources: () => { - return clickNestedMenuItem(by.cssContainingText('.dropdown-toggle', 'Resources'), by.cssContainingText('a', 'Other Resources')); + return clickNestedMenuItem( + by.cssContainingText(selectors.sidePrimary, 'Resources'), + by.cssContainingText(selectors.sideSecondary, 'Other Resources')); }, // the rest of the top lvl menu items clickStorage: () => { - return element(by.cssContainingText('a', 'Storage')).click(); + return element(by.cssContainingText(selectors.sidePrimary, 'Storage')).click(); }, clickMonitoring: () => { - return element(by.cssContainingText('a', 'Monitoring')).click(); + return element(by.cssContainingText(selectors.sidePrimary, 'Monitoring')).click(); } } }; diff --git a/test/integration/page-objects/modals/addTemplateModal.js b/test/integration/page-objects/modals/addTemplateModal.js index c986672e71..e6c857023c 100644 --- a/test/integration/page-objects/modals/addTemplateModal.js +++ b/test/integration/page-objects/modals/addTemplateModal.js @@ -1,6 +1,7 @@ 'use strict'; const inputs = require('../../helpers/inputs'); +const timing = require('../../helpers/timing'); class AddTemplateModal { constructor(project) { @@ -16,7 +17,7 @@ class AddTemplateModal { inputs.check(this.processBox); inputs.uncheck(this.saveBox); this.continue.click(); - return browser.sleep(500).then(() => { + return browser.sleep(timing.implicitRedirect).then(() => { // lazy require to avoid potential of circular dependencies let CreateFromTemplatePage = require('../createFromTemplate').CreateFromTemplatePage; return new CreateFromTemplatePage(this.project); @@ -26,7 +27,7 @@ class AddTemplateModal { inputs.uncheck(this.processBox); inputs.check(this.saveBox); this.continue.click(); - return browser.sleep(500).then(() => { + return browser.sleep(timing.implicitRedirect).then(() => { // lazy require let OverviewPage = require('../overview').OverviewPage; return new OverviewPage(this.project); // automatic redirect diff --git a/test/integration/page-objects/nextSteps.js b/test/integration/page-objects/nextSteps.js new file mode 100644 index 0000000000..1859ce6e79 --- /dev/null +++ b/test/integration/page-objects/nextSteps.js @@ -0,0 +1,18 @@ +'use strict'; + +const Page = require('./page').Page; + +class NextStepsPage extends Page { + constructor(project, menu) { + super(project, menu); + } + // NOTE: this page will typically be implicitly visited + // as the result of a redirect. The state is stored in + // the query string, for example: + // ?name=Node.js%20%2B%20MongoDB%20(Ephemeral) + getUrl() { + return 'project/' + this.project.name + '/create/next'; + } +} + +exports.NextStepsPage = NextStepsPage; diff --git a/test/integration/page-objects/page.js b/test/integration/page-objects/page.js index 25ace1cd45..2b7b330b30 100644 --- a/test/integration/page-objects/page.js +++ b/test/integration/page-objects/page.js @@ -1,6 +1,7 @@ 'use strict'; const h = require('../helpers'); +const timing = require('../helpers/timing'); const logger = require('../helpers/logger'); const defaultMenus = require('./menus'); @@ -8,13 +9,22 @@ class Page { constructor(project, menus) { this.project = project; this.menus = menus || defaultMenus; + // Whenever a page is created, we need to give + // it some time to render + browser.sleep(timing.standardDelay); } getUrl() { } + // Visit should only be used as the initial entry point to + // the app. After that, tests should use page.menu.click() + // to navigate. visit() { - logger.log('visiting url:', this.getUrl()); - return h.goToPage(this.getUrl()); + logger.log('Visiting url (refresh):', this.getUrl()); + return h.goToPage(this.getUrl()).then(() => { + // Calling visit performs a browser refresh each time. + browser.sleep(timing.initialVisit); + }); } } diff --git a/test/integration/page-objects/projectList.js b/test/integration/page-objects/projectList.js new file mode 100644 index 0000000000..bbac812f31 --- /dev/null +++ b/test/integration/page-objects/projectList.js @@ -0,0 +1,30 @@ +'use strict'; + +const wait = require('../helpers/wait'); +const Page = require('./page').Page; + +class ProjectList extends Page { + constructor(project, menu) { + super(project, menu); + } + getUrl() { + return 'projects'; + } + findProjectTiles() { + return element.all(by.css('.list-group-item')); + } + findTileBy(projectName) { + let elem = element(by.cssContainingText('.tile-target', projectName)); + wait.forElem(elem); + return elem; + } + clickTileBy(projectName) { + // This is an implicit redirect, which typically + // returns an instance of the new page. It should + // return new Overview(relevantProject), but this + // method doesn't currently take a full project object + return this.findTileBy(projectName).click(); + } +} + +exports.ProjectList = ProjectList; diff --git a/test/karma.conf.js b/test/karma.conf.js index a654dd76ae..dd6db5e605 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -2,6 +2,7 @@ // http://karma-runner.github.io/0.12/config/configuration-file.html // Generated on 2014-09-12 using // generator-karma 0.8.3 +let isMac = /^darwin/.test(process.platform); module.exports = function(config) { 'use strict'; @@ -141,7 +142,7 @@ module.exports = function(config) { // if true, it capture browsers, run tests and exit singleRun: false, - colors: true, + colors: isMac, // level of logging // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG diff --git a/test/protractor-mac.conf.js b/test/protractor-mac.conf.js deleted file mode 100644 index d195863900..0000000000 --- a/test/protractor-mac.conf.js +++ /dev/null @@ -1,19 +0,0 @@ -// Reference Configuration File -// -// This file shows all of the configuration options that may be passed -// to Protractor. -var _ = require('lodash'); -var baseConf = require('./protractor.conf').config; - -// overrides to the base protractor.conf to use gecko.driver on mac -exports.config = _.extend({}, baseConf, { - seleniumArgs: ['-Dwebdriver.gecko.driver=./node_modules/geckodriver/geckodriver'], - capabilities: { - name: 'Unnamed Job', - count: 1, - shardTestFiles: false, - maxInstances: 1, - marionette: true, - acceptInsecureCerts: true - } -}); diff --git a/test/protractor.conf.js b/test/protractor.conf.js index b8775115b9..06899762d1 100644 --- a/test/protractor.conf.js +++ b/test/protractor.conf.js @@ -1,340 +1,96 @@ -// Reference Configuration File -// -// This file shows all of the configuration options that may be passed -// to Protractor. +'use strict'; + +let isMac = /^darwin/.test(process.platform); +let SpecReporter = require('jasmine-spec-reporter').SpecReporter; +let HtmlScreenshotReporter = require('protractor-jasmine2-screenshot-reporter'); +let jasmineReporters = require('jasmine-reporters'); + +let screenshotReporter = new HtmlScreenshotReporter({ + cleanDestination: isMac ? true : false, + dest: './test/tmp/screenshots', + filename: 'protractor-e2e-report.html', + takeScreenShotsOnlyForFailedSpecs: true, + pathBuilder: function(currentSpec, suites, browserCapabilities) { + return browserCapabilities.get('browserName') + '/' + currentSpec.fullName; + } +}); +let junitReporter = new jasmineReporters.JUnitXmlReporter({ + consolidateAll: true, + savePath: 'test/junit', + filePrefix: 'e2e-results' +}); +// https://github.com/angular/protractor/blob/master/docs/browser-setup.md exports.config = { - // --------------------------------------------------------------------------- - // ----- How to connect to Browser Drivers ----------------------------------- - // --------------------------------------------------------------------------- - // - // Protractor needs to know how to connect to Drivers for the browsers - // it is testing on. This is usually done through a Selenium Server. - // There are four options - specify one of the following: - // - // 1. seleniumServerJar - to start a standalone Selenium Server locally. - // 2. seleniumAddress - to connect to a Selenium Server which is already - // running. - // 3. sauceUser/sauceKey - to use remote Selenium Servers via Sauce Labs. - // 4. directConnect - to connect directly to the browser Drivers. - // This option is only available for Firefox and Chrome. - - // ---- 1. To start a standalone Selenium Server locally --------------------- - // The location of the standalone Selenium Server jar file, relative - // to the location of this config. If no other method of starting Selenium - // Server is found, this will default to - // node_modules/protractor/selenium/selenium-server... - seleniumServerJar: null, - // The port to start the Selenium Server on, or null if the server should - // find its own unused port. Ignored if seleniumServerJar is null. - seleniumPort: null, - // Additional command line options to pass to selenium. For example, - // if you need to change the browser timeout, use - // seleniumArgs: ['-browserTimeout=60'] - // Ignored if seleniumServerJar is null. - seleniumArgs: [], - // ChromeDriver location is used to help find the chromedriver binary. - // This will be passed to the Selenium jar as the system property - // webdriver.chrome.driver. If null, Selenium will - // attempt to find ChromeDriver using PATH. - // chromeDriver: './selenium/chromedriver', - - // ---- 2. To connect to a Selenium Server which is already running ---------- - // The address of a running Selenium Server. If specified, Protractor will - // connect to an already running instance of Selenium. This usually looks like - // seleniumAddress: 'http://localhost:4444/wd/hub' - seleniumAddress: null, - - // ---- 3. To use remote browsers via Sauce Labs ----------------------------- - // If sauceUser and sauceKey are specified, seleniumServerJar will be ignored. - // The tests will be run remotely using Sauce Labs. - sauceUser: null, - sauceKey: null, - // Use sauceSeleniumAddress if you need to customize the URL Protractor - // uses to connect to sauce labs (for example, if you are tunneling selenium - // traffic through a sauce connect tunnel). Default is - // ondemand.saucelabs.com:80/wd/hub - sauceSeleniumAddress: null, - - // ---- 4. To connect directly to Drivers ------------------------------------ - // Boolean. If true, Protractor will connect directly to the browser Drivers - // at the locations specified by chromeDriver and firefoxPath. Only Chrome - // and Firefox are supported for direct connect. - directConnect: false, - // Path to the firefox application binary. If null, will attempt to find - // firefox in the default locations. - firefoxPath: null, - - // **DEPRECATED** - // If true, only ChromeDriver will be started, not a Selenium Server. - // This should be replaced with directConnect. - chromeOnly: false, - - // --------------------------------------------------------------------------- - // ----- What tests to run --------------------------------------------------- - // --------------------------------------------------------------------------- - - // Spec patterns are relative to the location of this config. + // specs is here to define an order when all tests are run. + // use suites below if you want to run individual tests. specs: [ - 'integration/**/*.js' + // create project first + 'integration/features/user_creates_project.spec.js', + // then the more complex tests + 'integration/features/user_adds_template_to_project.spec.js', + 'integration/features/user_adds_imagestream_to_project.spec.js', + 'integration/features/user_creates_from_url.spec.js' ], - - // Patterns to exclude. - exclude: ['integration/e2e.js'], // We are temporarily excluding the e2e tests while we transition to the split merge queue - - // Alternatively, suites may be used. When run without a command line - // parameter, all suites will run. If run with --suite=create-project or - // --suite=create-projct,add-template-to-project, only the patterns matched by - // the specified suites will run. + // this test is for debugging the login flow only. since all the + // tests exercise the login flow, it should not need to be run regularly. + exclude: ['integration/features/user_logs_in.spec.js'], + // usage: + // grunt test-e2e + // grunt test-integration + // single suite: + // grunt test-integration --suite=create-project + // set of suites: + // grunt test-integration --suite=create-projct,add-template-to-project suites: { - 'create-project': 'integration/features/user_creates_project.spec.js', // This suite of tests should only require a running master api, it should not require a node + 'create-project': 'integration/features/user_creates_project.spec.js', 'add-template-to-project': 'integration/features/user_adds_template_to_project.spec.js', 'add-imagestream-to-project': 'integration/features/user_adds_imagestream_to_project.spec.js', 'create-from-url': 'integration/features/user_creates_from_url.spec.js', - // e2e: 'integration/e2e.js' + // simple test to ensure we can get past OAuth + 'login': 'integration/features/user_logs_in.spec.js' }, - - // --------------------------------------------------------------------------- - // ----- How to set up browsers ---------------------------------------------- - // --------------------------------------------------------------------------- - // - // Protractor can launch your tests on one or more browsers. If you are - // testing on a single browser, use the capabilities option. If you are - // testing on multiple browsers, use the multiCapabilities array. - - // For a list of available capabilities, see - // https://code.google.com/p/selenium/wiki/DesiredCapabilities - // - // In addition, you may specify count, shardTestFiles, and maxInstances. - capabilities: { - // browserName: 'chrome', - - // Name of the process executing this capability. Not used directly by - // protractor or the browser, but instead pass directly to third parties - // like SauceLabs as the name of the job running this test - name: 'Unnamed Job', - - // Number of times to run this set of capabilities (in parallel, unless - // limited by maxSessions). Default is 1. - count: 1, - - // If this is set to be true, specs will be sharded by file (i.e. all - // files to be run by this set of capabilities will run in parallel). - // Default is false. - shardTestFiles: false, - - // Maximum number of browser instances that can run in parallel for this - // set of capabilities. This is only needed if shardTestFiles is true. - // Default is 1. - maxInstances: 1, - - // Additional spec files to be run on this capability only. - // specs: ['spec/chromeOnlySpec.js'], - - // Spec files to be excluded on this capability only. - // exclude: ['spec/doNotRunInChromeSpec.js'], - - // Optional: override global seleniumAddress on this capability only. - // seleniumAddress: null - }, - - // If you would like to run more than one instance of WebDriver on the same - // tests, use multiCapabilities, which takes an array of capabilities. - // If this is specified, capabilities will be ignored. - // multiCapabilities: [], - - // If you need to resolve multiCapabilities asynchronously (i.e. wait for - // server/proxy, set firefox profile, etc), you can specify a function here - // which will return either `multiCapabilities` or a promise to - // `multiCapabilities`. - // If this returns a promise, it is resolved immediately after - // `beforeLaunch` is run, and before any driver is set up. - // If this is specified, both capabilities and multiCapabilities will be - // ignored. - getMultiCapabilities: null, - - // Maximum number of total browser sessions to run. Tests are queued in - // sequence if number of browser sessions is limited by this parameter. - // Use a number less than 1 to denote unlimited. Default is unlimited. - maxSessions: -1, - - // --------------------------------------------------------------------------- - // ----- Global test information --------------------------------------------- - // --------------------------------------------------------------------------- - // - // A base URL for your application under test. Calls to protractor.get() - // with relative paths will be prepended with this. - // baseUrl: 'https://localhost:9000', - - // CSS Selector for the element housing the angular app - this defaults to - // body, but is necessary if ng-app is on a descendant of . - rootElement: 'body', - - // The timeout in milliseconds for each script run on the browser. This should - // be longer than the maximum time your application needs to stabilize between - // tasks. + framework: 'jasmine2', allScriptsTimeout: 30 * 1000, - - // How long to wait for a page to load. getPageTimeout: 30 * 1000, - - // A callback function called once configs are read but before any environment - // setup. This will only run once, and before onPrepare. - // You can specify a file containing code to run by setting beforeLaunch to - // the filename string. - beforeLaunch: function() { - // At this point, global variable 'protractor' object will NOT be set up, - // and globals from the test framework will NOT be available. The main - // purpose of this function should be to bring up test dependencies. + jasmineNodeOpts: { + defaultTimeoutInterval: 60 * 1000, + isVerbose: true, + includeStackTrace: true, + showColors: isMac, + // noop to eliminate the dot reporter, since we have + // better reporters. see onPrepare below + print: function() {} }, - - // A callback function called once protractor is ready and available, and - // before the specs are executed. - // If multiple capabilities are being run, this will run once per - // capability. - // You can specify a file containing code to run by setting onPrepare to - // the filename string. + capabilities: { + 'browserName': 'chrome', + }, + // `grunt test-integration` task config overrides multiCapabilties, + // ideally we would use this to test multiple browsers at once. + // multiCapabilities: [ + // // {'browserName': 'firefox'}, + // // {'browserName': 'chrome'} + // // {'browserName': 'phantomjs'} + // ], onPrepare: function() { - // At this point, global variable 'protractor' object will be set up, and - // globals from the test framework will be available. For example, if you - // are using Jasmine, you can add a reporter with: - // jasmine.getEnv().addReporter(new jasmine.JUnitXmlReporter( - // 'outputdir/', true, true)); - // - // If you need access back to the current configuration object, - // use a pattern like the following: - // browser.getProcessedConfig().then(function(config) { - // // config.capabilities is the CURRENT capability being run, if - // // you are using multiCapabilities. - // console.log('Executing capability', config.capabilities); - // }); + jasmine.getEnv().addReporter(screenshotReporter); - var SpecReporter = require('jasmine-spec-reporter'); - // add jasmine spec reporter jasmine.getEnv().addReporter(new SpecReporter({ displayStacktrace: true, - displaySuccessfulSpec: false, // display each successful spec - displayFailedSpec: false // display each failed spec - })); - - var ScreenShotReporter = require('protractor-screenshot-reporter'); - // Add a screenshot reporter and store screenshots to `/tmp/screnshots`: - jasmine.getEnv().addReporter(new ScreenShotReporter({ - baseDirectory: './test/tmp/screenshots', - takeScreenShotsOnlyForFailedSpecs: true, - pathBuilder: function pathBuilder(spec, descriptions, results, capabilities) { - // Return '' as path for screenshots: - // Example: 'list-should work'. - return descriptions.reverse().join(' ').replace(/[^a-zA-Z0-9 ]/g, "").trim(); - } + displaySuccessfulSpec: false, + displayFailedSpec: true })); - }, - - // A callback function called once tests are finished. - onComplete: function() { - // At this point, tests will be done but global objects will still be - // available. - }, - - // A callback function called once the tests have finished running and - // the WebDriver instance has been shut down. It is passed the exit code - // (0 if the tests passed). This is called once per capability. - onCleanUp: function(exitCode) {}, - - // A callback function called once all tests have finished running and - // the WebDriver instance has been shut down. It is passed the exit code - // (0 if the tests passed). This is called only once before the program - // exits (after onCleanUp). - afterLaunch: function() {}, - - // The params object will be passed directly to the Protractor instance, - // and can be accessed from your test as browser.params. It is an arbitrary - // object and can contain anything you may need in your test. - // This can be changed via the command line as: - // --params.login.user 'Joe' - params: { - login: { - user: 'Jane', - password: '1234' - } - }, - - // If set, protractor will save the test output in json format at this path. - // The path is relative to the location of this config. - resultJsonOutputFile: null, - - // If true, protractor will restart the browser between each test. - // CAUTION: This will cause your tests to slow down drastically. - restartBrowserBetweenTests: false, - - // --------------------------------------------------------------------------- - // ----- The test framework -------------------------------------------------- - // --------------------------------------------------------------------------- - - // Test framework to use. This may be one of: - // jasmine, jasmine2, cucumber, mocha or custom. - // - // When the framework is set to "custom" you'll need to additionally - // set frameworkPath with the path relative to the config file or absolute - // framework: 'custom', - // frameworkPath: './frameworks/my_custom_jasmine.js', - // See github.com/angular/protractor/blob/master/lib/frameworks/README.md - // to comply with the interface details of your custom implementation. - // - // Jasmine is fully supported as a test and assertion framework. - // Mocha and Cucumber have limited beta support. You will need to include your - // own assertion framework (such as Chai) if working with Mocha. - framework: 'jasmine', - - // Options to be passed to minijasminenode. - // - // See the full list at https://github.com/juliemr/minijasminenode/tree/jasmine1 - jasmineNodeOpts: { - // If true, display spec names. - isVerbose: false, - // If true, print colors to the terminal. - showColors: true, - // If true, include stack traces in failures. - includeStackTrace: true, - // Default time to wait in ms before a test fails. - defaultTimeoutInterval: 60000 - }, - // Options to be passed to jasmine2. - // - // See https://github.com/jasmine/jasmine-npm/blob/master/lib/jasmine.js - // for the exact options available. - jasmineNodeOpts: { - // If true, print colors to the terminal. - showColors: true, - // Default time to wait in ms before a test fails. - defaultTimeoutInterval: 60000, - // Function called to print jasmine results. - print: function() {}, - // If set, only execute specs whose names match the pattern, which is - // internally compiled to a RegExp. - grep: 'pattern', - // Inverts 'grep' matches - invertGrep: false + jasmine.getEnv().addReporter(junitReporter); }, - - // Options to be passed to Mocha. - // - // See the full list at http://mochajs.org/ - mochaOpts: { - ui: 'bdd', - reporter: 'list' - }, - - // Options to be passed to Cucumber. - cucumberOpts: { - // Require files before executing the features. - require: 'cucumber/stepDefinitions.js', - // Only execute the features or scenarios with tags matching @dev. - // This may be an array of strings to specify multiple tags to include. - tags: '@dev', - // How to format features (default: progress) - format: 'summary' + beforeLaunch: function() { + // this should force the screenshot reporter to take a screenshot + // if an exception is thrown from within a test that isn't a test error + // https://github.com/mlison/protractor-jasmine2-screenshot-reporter#tips--tricks + process.on('uncaughtException', function () { + screenshotReporter.jasmineDone(); + screenshotReporter.afterLaunch(); + }); } }; From ae08240f878915cca19e000fb52b02cd727b5e7f Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 18 Oct 2017 09:17:42 -0400 Subject: [PATCH 02/10] f: add wait statements to matchers --- test/integration/helpers/matchers.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/helpers/matchers.js b/test/integration/helpers/matchers.js index 9bee4b4e4f..8cc8100ad2 100644 --- a/test/integration/helpers/matchers.js +++ b/test/integration/helpers/matchers.js @@ -31,10 +31,12 @@ exports.expectElementToExist = (elem) => { }; exports.expectElementToBeVisible = (elem) => { + wait.forElem(elem); expect(elem.isDisplayed()).toBeTruthy(); }; exports.expectElementToBeHidden = (elem) => { + wait.forElem(elem); expect(elem.isDisplayed()).toBeFalsy(); }; From 6eacca6a589de472117771e9728d25e086373c40 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 18 Oct 2017 10:56:13 -0400 Subject: [PATCH 03/10] WIP: projectHelpers.deleteAllProjects() seems to be problemating for our flow --- test/integration/helpers/project.js | 44 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/test/integration/helpers/project.js b/test/integration/helpers/project.js index 83e2244f10..2c1dfd13f3 100644 --- a/test/integration/helpers/project.js +++ b/test/integration/helpers/project.js @@ -1,10 +1,12 @@ 'use strict'; -const h = require('../helpers.js'); +const deprecatedHelpers = require('../helpers'); +const wait = require('./wait'); +const logger = require('./logger'); // TODO: factor this out into a proper page object exports.visitCreatePage = () => { - h.goToPage('create-project'); + deprecatedHelpers.goToPage('create-project'); }; exports.projectDetails = () => { @@ -19,21 +21,21 @@ exports.projectDetails = () => { exports.createProject = (project, uri) => { for (let key in project) { - h.setInputValue(key, project[key]); + deprecatedHelpers.setInputValue(key, project[key]); } - h.clickAndGo('Create', uri); + deprecatedHelpers.clickAndGo('Create', uri); }; exports.deleteProject = (project) => { - h.goToPage('projects'); + deprecatedHelpers.goToPage('projects'); let projectTile = element(by.cssContainingText(".project-info", project['name'])); projectTile.element(by.css('.dropdown-toggle')).click(); projectTile.element(by.linkText('Delete Project')).click(); - h.setInputValue('confirmName', project.name); + deprecatedHelpers.setInputValue('confirmName', project.name); let deleteButton = element(by.cssContainingText(".modal-dialog .btn", "Delete")); browser.wait(protractor.ExpectedConditions.elementToBeClickable(deleteButton), 2000); deleteButton.click(); - h.waitForPresence(".alert-success", "marked for deletion"); + deprecatedHelpers.waitForPresence(".alert-success", "marked for deletion"); }; // All projects visible to the current user. @@ -41,7 +43,8 @@ exports.deleteProject = (project) => { // Be careful about using this function if your test gives the e2e-user access // to internal projects such as openshift, or openshift-infra exports.deleteAllProjects = () => { - h.goToPage('projects'); + return; + deprecatedHelpers.goToPage('projects'); let projectTiles = element.all(by.css(".project-info")); let allDeleted = protractor.promise.defer(); let numDeleted = 0; @@ -52,22 +55,31 @@ exports.deleteAllProjects = () => { if(count === 0) { allDeleted.fulfill(); } + logger.log('LOGGER: projects to delete:', count); }); - projectTiles.each((elem) => { + projectTiles.each((elem, index) => { + logger.log('LOGGER: deleting', index); let projectTitle = elem.element(by.css('.tile-target span')).getText(); elem.element(by.css('.dropdown-toggle')).click(); elem.element(by.linkText('Delete Project')).click(); - h.setInputValue('confirmName', projectTitle); - // then click delete - let modal = element(by.css('.modal-dialog')); - let deleteButton = modal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); - browser.wait(protractor.ExpectedConditions.elementToBeClickable(deleteButton), 2000); + deprecatedHelpers.setInputValue('confirmName', projectTitle); + + let deleteModal = element(by.css('.delete-resource-modal')); + wait.forElem(deleteModal); + let deleteButton = deleteModal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); + wait.forClickableElem(deleteButton); deleteButton.click(); - h.waitForPresence(".alert-success", "marked for deletion"); - h.waitForElemRemoval(element(by.css('.modal-dialog'))); + + // let projectMarkedForDeletionToast = element( + // by.cssContainingText( + // '.toast-pf.alert.alert-success', + // `Project ${projectTitle} was marked for deletion.`)); + // wait.forElem(projectMarkedForDeletionToast); + numDeleted++; if(numDeleted >= count) { + logger.log('LOGGER: welp, must be done? (1)', numDeleted, count); allDeleted.fulfill(numDeleted); } }); From 37597d3a27c3a0ad82a504845a6e7d384d2681b6 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 18 Oct 2017 11:31:37 -0400 Subject: [PATCH 04/10] WIP: comment out deleteAllProjects() helper --- test/integration/helpers/project.js | 84 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/test/integration/helpers/project.js b/test/integration/helpers/project.js index 2c1dfd13f3..021544e634 100644 --- a/test/integration/helpers/project.js +++ b/test/integration/helpers/project.js @@ -1,8 +1,8 @@ 'use strict'; const deprecatedHelpers = require('../helpers'); -const wait = require('./wait'); -const logger = require('./logger'); +// const wait = require('./wait'); +// const logger = require('./logger'); // TODO: factor this out into a proper page object exports.visitCreatePage = () => { @@ -44,44 +44,44 @@ exports.deleteProject = (project) => { // to internal projects such as openshift, or openshift-infra exports.deleteAllProjects = () => { return; - deprecatedHelpers.goToPage('projects'); - let projectTiles = element.all(by.css(".project-info")); - let allDeleted = protractor.promise.defer(); - let numDeleted = 0; - let count; - projectTiles.count().then((num) => { - count = num; - // safely fulfill if there happen to be no projects. - if(count === 0) { - allDeleted.fulfill(); - } - logger.log('LOGGER: projects to delete:', count); - }); - - projectTiles.each((elem, index) => { - logger.log('LOGGER: deleting', index); - let projectTitle = elem.element(by.css('.tile-target span')).getText(); - elem.element(by.css('.dropdown-toggle')).click(); - elem.element(by.linkText('Delete Project')).click(); - deprecatedHelpers.setInputValue('confirmName', projectTitle); - - let deleteModal = element(by.css('.delete-resource-modal')); - wait.forElem(deleteModal); - let deleteButton = deleteModal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); - wait.forClickableElem(deleteButton); - deleteButton.click(); - - // let projectMarkedForDeletionToast = element( - // by.cssContainingText( - // '.toast-pf.alert.alert-success', - // `Project ${projectTitle} was marked for deletion.`)); - // wait.forElem(projectMarkedForDeletionToast); - - numDeleted++; - if(numDeleted >= count) { - logger.log('LOGGER: welp, must be done? (1)', numDeleted, count); - allDeleted.fulfill(numDeleted); - } - }); - return allDeleted.promise; + // deprecatedHelpers.goToPage('projects'); + // let projectTiles = element.all(by.css(".project-info")); + // let allDeleted = protractor.promise.defer(); + // let numDeleted = 0; + // let count; + // projectTiles.count().then((num) => { + // count = num; + // // safely fulfill if there happen to be no projects. + // if(count === 0) { + // allDeleted.fulfill(); + // } + // logger.log('LOGGER: projects to delete:', count); + // }); + // + // projectTiles.each((elem, index) => { + // logger.log('LOGGER: deleting', index); + // let projectTitle = elem.element(by.css('.tile-target span')).getText(); + // elem.element(by.css('.dropdown-toggle')).click(); + // elem.element(by.linkText('Delete Project')).click(); + // deprecatedHelpers.setInputValue('confirmName', projectTitle); + // + // let deleteModal = element(by.css('.delete-resource-modal')); + // wait.forElem(deleteModal); + // let deleteButton = deleteModal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); + // wait.forClickableElem(deleteButton); + // deleteButton.click(); + // + // // let projectMarkedForDeletionToast = element( + // // by.cssContainingText( + // // '.toast-pf.alert.alert-success', + // // `Project ${projectTitle} was marked for deletion.`)); + // // wait.forElem(projectMarkedForDeletionToast); + // + // numDeleted++; + // if(numDeleted >= count) { + // logger.log('LOGGER: welp, must be done? (1)', numDeleted, count); + // allDeleted.fulfill(numDeleted); + // } + // }); + // return allDeleted.promise; }; From 28c2f558810765cf3ede7dce7de769fce1f70b6c Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 25 Oct 2017 14:31:50 -0400 Subject: [PATCH 05/10] Update beforeAll,afterAll,beforeEach,afterEach --- test/integration/environment.js | 3 ++- .../user_adds_imagestream_to_project.spec.js | 8 ++++++++ .../features/user_adds_template_to_project.spec.js | 8 ++++++++ .../features/user_creates_from_url.spec.js | 2 ++ .../features/user_creates_project.spec.js | 11 +++++++++++ test/integration/helpers/common.js | 13 ++++++++++++- 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/test/integration/environment.js b/test/integration/environment.js index 4e9f423bfb..210ea7fa46 100644 --- a/test/integration/environment.js +++ b/test/integration/environment.js @@ -1,4 +1,5 @@ module.exports = { baseUrl: 'https://localhost:9000/dev-console/', - loginUrl: 'https://127.0.0.1:8443/login' + loginUrl: 'https://127.0.0.1:8443/login', + isMac: /^darwin/.test(process.platform) }; diff --git a/test/integration/features/user_adds_imagestream_to_project.spec.js b/test/integration/features/user_adds_imagestream_to_project.spec.js index 9d9581ead0..88b0ccc031 100644 --- a/test/integration/features/user_adds_imagestream_to_project.spec.js +++ b/test/integration/features/user_adds_imagestream_to_project.spec.js @@ -20,6 +20,14 @@ describe('User adds an image stream to a project', () => { common.afterEach(); }); + beforeAll(() => { + common.beforeAll(); + }); + + afterAll(() => { + common.afterAll(); + }); + describe('after creating a new project', () => { describe('using the Import YAML tab', () => { it('should process and create the images in the image stream', () => { diff --git a/test/integration/features/user_adds_template_to_project.spec.js b/test/integration/features/user_adds_template_to_project.spec.js index 14f8fea07f..99a1069a83 100644 --- a/test/integration/features/user_adds_template_to_project.spec.js +++ b/test/integration/features/user_adds_template_to_project.spec.js @@ -22,6 +22,14 @@ describe('User adds a template to a project', () => { common.afterEach(); }); + beforeAll(() => { + common.beforeAll(); + }); + + afterAll(() => { + common.afterAll(); + }); + // TODO: the expect() statements below are using hard-coded values // rather than testing against the fixture itself. This is fine for // now, but if we ever update the fixture the tests will likely break. diff --git a/test/integration/features/user_creates_from_url.spec.js b/test/integration/features/user_creates_from_url.spec.js index eb029ab39d..d4f70a0b96 100644 --- a/test/integration/features/user_creates_from_url.spec.js +++ b/test/integration/features/user_creates_from_url.spec.js @@ -45,12 +45,14 @@ describe('authenticated e2e-user', function() { // NOTE: beforeAll vs beforeEach. // these tests only do the setup once. beforeAll(() => { + common.beforeAll(); common.beforeEach(); setupEnv(); }); afterAll(() => { common.afterEach(); + common.afterAll(); resetExtensions(); }); diff --git a/test/integration/features/user_creates_project.spec.js b/test/integration/features/user_creates_project.spec.js index 1f223d6938..509bdf5ee0 100644 --- a/test/integration/features/user_creates_project.spec.js +++ b/test/integration/features/user_creates_project.spec.js @@ -21,6 +21,17 @@ describe('Authenticated user creates a new project', () => { common.afterEach(); }); + // NOTE: beforeAll vs beforeEach. + // these tests only do the setup once. + beforeAll(() => { + common.beforeAll(); + }); + + afterAll(() => { + common.afterAll(); + }); + + it('should be able to create a new project', () => { let project = projectHelpers.projectDetails(); diff --git a/test/integration/helpers/common.js b/test/integration/helpers/common.js index 782ba5a099..d1ea941fcf 100644 --- a/test/integration/helpers/common.js +++ b/test/integration/helpers/common.js @@ -1,5 +1,6 @@ 'use strict'; +const environment = require('../environment'); const windowHelper = require('../helpers/window'); const projectHelpers = require('../helpers/project'); const timing = require('../helpers/timing'); @@ -9,6 +10,17 @@ const LoginPage = require('../page-objects/login').LoginPage; exports.beforeAll = () => { windowHelper.setSize(); + if(environment.isMac) { + logger.log('local env: deleting all projects...'); + projectHelpers.deleteAllProjects(); + } +}; + +exports.afterAll = () => { + if(environment.isMac) { + logger.log('local env: deleting all projects...'); + projectHelpers.deleteAllProjects(); + } }; exports.beforeEach = () => { @@ -19,7 +31,6 @@ exports.beforeEach = () => { let loginPage = new LoginPage(); loginPage.login(); browser.driver.sleep(timing.standardDelay); - projectHelpers.deleteAllProjects(); }; From 4a0b1559c42154d475d5b01b9a441627ec99d70c Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Wed, 25 Oct 2017 21:18:46 -0400 Subject: [PATCH 06/10] Add user_deletes_all_projects.spec.js (empty) --- .../features/user_deletes_all_projects.spec.js | 0 test/integration/helpers/common.js | 2 ++ test/integration/helpers/timing.js | 3 ++- test/integration/helpers/window.js | 2 +- test/protractor.conf.js | 9 ++++++--- 5 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 test/integration/features/user_deletes_all_projects.spec.js diff --git a/test/integration/features/user_deletes_all_projects.spec.js b/test/integration/features/user_deletes_all_projects.spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/integration/helpers/common.js b/test/integration/helpers/common.js index d1ea941fcf..88de44e269 100644 --- a/test/integration/helpers/common.js +++ b/test/integration/helpers/common.js @@ -9,6 +9,7 @@ const menus = require('../page-objects/menus').menus; const LoginPage = require('../page-objects/login').LoginPage; exports.beforeAll = () => { + windowHelper.setSize(); if(environment.isMac) { logger.log('local env: deleting all projects...'); @@ -21,6 +22,7 @@ exports.afterAll = () => { logger.log('local env: deleting all projects...'); projectHelpers.deleteAllProjects(); } + browser.sleep(timing.pauseBetweenTests); }; exports.beforeEach = () => { diff --git a/test/integration/helpers/timing.js b/test/integration/helpers/timing.js index 0a945c87c8..9d3c6d499c 100644 --- a/test/integration/helpers/timing.js +++ b/test/integration/helpers/timing.js @@ -20,5 +20,6 @@ module.exports = { // timing.waitForElement, // 'Elem did not appear') waitForElement: 1000, - maxWaitForElement: 15000 + maxWaitForElement: 15 * 1000, + pauseBetweenTests: 5 * 1000 }; diff --git a/test/integration/helpers/window.js b/test/integration/helpers/window.js index d5c59b1241..b20f64ee70 100644 --- a/test/integration/helpers/window.js +++ b/test/integration/helpers/window.js @@ -2,7 +2,7 @@ const timing = require('./timing'); -exports.setSize = (height = 2024, width = 2048) => { +exports.setSize = (height = 768, width = 1024) => { browser.driver.manage().window().setSize(width, height); }; diff --git a/test/protractor.conf.js b/test/protractor.conf.js index 06899762d1..c790187475 100644 --- a/test/protractor.conf.js +++ b/test/protractor.conf.js @@ -33,9 +33,11 @@ exports.config = { 'integration/features/user_adds_imagestream_to_project.spec.js', 'integration/features/user_creates_from_url.spec.js' ], - // this test is for debugging the login flow only. since all the - // tests exercise the login flow, it should not need to be run regularly. - exclude: ['integration/features/user_logs_in.spec.js'], + // helper tests for local debuggin, these should not need to be run in CI + exclude: [ + 'integration/features/user_logs_in.spec.js', + 'integration/features/user_deletes_all_projects.spec.js' + ], // usage: // grunt test-e2e // grunt test-integration @@ -48,6 +50,7 @@ exports.config = { 'add-template-to-project': 'integration/features/user_adds_template_to_project.spec.js', 'add-imagestream-to-project': 'integration/features/user_adds_imagestream_to_project.spec.js', 'create-from-url': 'integration/features/user_creates_from_url.spec.js', + 'delete-all-projects': 'integration/features/user_deletes_all_projects.spec.js', // simple test to ensure we can get past OAuth 'login': 'integration/features/user_logs_in.spec.js' }, From 37ae78dae6a05fb558170195dee725c2cedb42e3 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Thu, 26 Oct 2017 23:41:43 -0400 Subject: [PATCH 07/10] Update delete projects flow --- .../user_adds_imagestream_to_project.spec.js | 10 +- .../user_adds_template_to_project.spec.js | 9 +- .../features/user_creates_project.spec.js | 32 +++- .../user_deletes_all_projects.spec.js | 28 ++++ test/integration/helpers/common.js | 38 ++--- test/integration/helpers/project.js | 81 +++++----- .../integration/page-objects/createProject.js | 4 +- test/integration/page-objects/menus.js | 13 ++ test/integration/page-objects/projectList.js | 138 +++++++++++++++++- test/protractor.conf.js | 16 +- 10 files changed, 284 insertions(+), 85 deletions(-) diff --git a/test/integration/features/user_adds_imagestream_to_project.spec.js b/test/integration/features/user_adds_imagestream_to_project.spec.js index 88b0ccc031..549f5df6b4 100644 --- a/test/integration/features/user_adds_imagestream_to_project.spec.js +++ b/test/integration/features/user_adds_imagestream_to_project.spec.js @@ -12,6 +12,10 @@ const centosImageStream = require('../fixtures/image-streams-centos7.json'); describe('User adds an image stream to a project', () => { + beforeAll(() => { + common.beforeAll(); + }); + beforeEach(() => { common.beforeEach(); }); @@ -20,14 +24,10 @@ describe('User adds an image stream to a project', () => { common.afterEach(); }); - beforeAll(() => { - common.beforeAll(); - }); - afterAll(() => { common.afterAll(); }); - + describe('after creating a new project', () => { describe('using the Import YAML tab', () => { it('should process and create the images in the image stream', () => { diff --git a/test/integration/features/user_adds_template_to_project.spec.js b/test/integration/features/user_adds_template_to_project.spec.js index 99a1069a83..e10f2e63fe 100644 --- a/test/integration/features/user_adds_template_to_project.spec.js +++ b/test/integration/features/user_adds_template_to_project.spec.js @@ -14,6 +14,11 @@ const nodeMongoTemplate = require('../fixtures/nodejs-mongodb'); describe('User adds a template to a project', () => { + + beforeAll(() => { + common.beforeAll(); + }); + beforeEach(() => { common.beforeEach(); }); @@ -22,10 +27,6 @@ describe('User adds a template to a project', () => { common.afterEach(); }); - beforeAll(() => { - common.beforeAll(); - }); - afterAll(() => { common.afterAll(); }); diff --git a/test/integration/features/user_creates_project.spec.js b/test/integration/features/user_creates_project.spec.js index 509bdf5ee0..67923ed277 100644 --- a/test/integration/features/user_creates_project.spec.js +++ b/test/integration/features/user_creates_project.spec.js @@ -8,11 +8,16 @@ const matchers = require('../helpers/matchers'); const CreateProjectPage = require('../page-objects/createProject').CreateProjectPage; const OverviewPage = require('../page-objects/overview').OverviewPage; +const ProjectListPage = require('../page-objects/projectList').ProjectListPage; const menus = require('../page-objects/menus').menus; describe('Authenticated user creates a new project', () => { + beforeAll(() => { + common.beforeAll(); + }); + beforeEach(() => { common.beforeEach(); }); @@ -21,12 +26,6 @@ describe('Authenticated user creates a new project', () => { common.afterEach(); }); - // NOTE: beforeAll vs beforeEach. - // these tests only do the setup once. - beforeAll(() => { - common.beforeAll(); - }); - afterAll(() => { common.afterAll(); }); @@ -102,6 +101,27 @@ describe('Authenticated user creates a new project', () => { // Documentation link leaves console // Copy Login should also just have its own test + // go to the project list page + menus.clickLogo(); + browser.sleep(timing.navToPage); + menus.clickViewAllProjects(); + browser.sleep(timing.navToPage); + let projectList2 = new ProjectListPage(); + + projectList2.clickTile(project); + browser.sleep(timing.navToPage); + menus.backToPreviousPage(); + + // projectList2.editProject(project); + // browser.sleep(1000); + // menus.backToPreviousPage(); + + projectList2.viewMembership(project); + browser.sleep(1000); + menus.backToPreviousPage(); + + projectList2.deleteProject(project); + }); }); }); diff --git a/test/integration/features/user_deletes_all_projects.spec.js b/test/integration/features/user_deletes_all_projects.spec.js index e69de29bb2..386005b905 100644 --- a/test/integration/features/user_deletes_all_projects.spec.js +++ b/test/integration/features/user_deletes_all_projects.spec.js @@ -0,0 +1,28 @@ +'use strict'; + +const common = require('../helpers/common'); +const ProjectListPage = require('../page-objects/projectList').ProjectListPage; + +// this test is technically just a helper to fix problems +// when testing locally. Run as: +// grunt test-integration --suite=delete-all-projects +describe('Authenticated user deletes all projects', () => { + + beforeAll(() => { + common.beforeAll(); + }); + + it('should delete each project one by one', () => { + + let projectList = new ProjectListPage(); + projectList.visit(); + + projectList.deleteAllProjects(); + + let tilesRemaining = element.all(by.css('.list-group-item.project-info')); + + tilesRemaining.count().then((num) => { + expect(num).toEqual(0); + }); + }); +}); diff --git a/test/integration/helpers/common.js b/test/integration/helpers/common.js index 88de44e269..577a3299a1 100644 --- a/test/integration/helpers/common.js +++ b/test/integration/helpers/common.js @@ -2,30 +2,15 @@ const environment = require('../environment'); const windowHelper = require('../helpers/window'); -const projectHelpers = require('../helpers/project'); const timing = require('../helpers/timing'); const logger = require('../helpers/logger'); const menus = require('../page-objects/menus').menus; const LoginPage = require('../page-objects/login').LoginPage; +const ProjectListPage = require('../page-objects/projectList').ProjectListPage; exports.beforeAll = () => { - windowHelper.setSize(); - if(environment.isMac) { - logger.log('local env: deleting all projects...'); - projectHelpers.deleteAllProjects(); - } -}; -exports.afterAll = () => { - if(environment.isMac) { - logger.log('local env: deleting all projects...'); - projectHelpers.deleteAllProjects(); - } - browser.sleep(timing.pauseBetweenTests); -}; - -exports.beforeEach = () => { logger.log('login'); // we manually bootstrap angular, so it is suggested to do this // call up front, however it has not been needed thus far. @@ -35,10 +20,29 @@ exports.beforeEach = () => { browser.driver.sleep(timing.standardDelay); }; +exports.afterAll = () => { + + // easier in the afterAll as we have done the login flow by this point. + if(environment.isMac) { + logger.log('cleanup: deleting all projects'); + menus.clickLogo(); + menus.clickViewAllProjects(); + let projectList = new ProjectListPage(); + projectList.deleteAllProjects(); + } + browser.sleep(timing.pauseBetweenTests); -exports.afterEach = () => { menus.topNav.clickLogout(); browser.sleep(timing.standardDelay); logger.log('logout'); windowHelper.clearStorage(); }; + +exports.beforeEach = () => { + +}; + + +exports.afterEach = () => { + +}; diff --git a/test/integration/helpers/project.js b/test/integration/helpers/project.js index 021544e634..6790e5d022 100644 --- a/test/integration/helpers/project.js +++ b/test/integration/helpers/project.js @@ -43,45 +43,44 @@ exports.deleteProject = (project) => { // Be careful about using this function if your test gives the e2e-user access // to internal projects such as openshift, or openshift-infra exports.deleteAllProjects = () => { - return; - // deprecatedHelpers.goToPage('projects'); - // let projectTiles = element.all(by.css(".project-info")); - // let allDeleted = protractor.promise.defer(); - // let numDeleted = 0; - // let count; - // projectTiles.count().then((num) => { - // count = num; - // // safely fulfill if there happen to be no projects. - // if(count === 0) { - // allDeleted.fulfill(); - // } - // logger.log('LOGGER: projects to delete:', count); - // }); - // - // projectTiles.each((elem, index) => { - // logger.log('LOGGER: deleting', index); - // let projectTitle = elem.element(by.css('.tile-target span')).getText(); - // elem.element(by.css('.dropdown-toggle')).click(); - // elem.element(by.linkText('Delete Project')).click(); - // deprecatedHelpers.setInputValue('confirmName', projectTitle); - // - // let deleteModal = element(by.css('.delete-resource-modal')); - // wait.forElem(deleteModal); - // let deleteButton = deleteModal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); - // wait.forClickableElem(deleteButton); - // deleteButton.click(); - // - // // let projectMarkedForDeletionToast = element( - // // by.cssContainingText( - // // '.toast-pf.alert.alert-success', - // // `Project ${projectTitle} was marked for deletion.`)); - // // wait.forElem(projectMarkedForDeletionToast); - // - // numDeleted++; - // if(numDeleted >= count) { - // logger.log('LOGGER: welp, must be done? (1)', numDeleted, count); - // allDeleted.fulfill(numDeleted); - // } - // }); - // return allDeleted.promise; + deprecatedHelpers.goToPage('projects'); + let projectTiles = element.all(by.css(".project-info")); + let allDeleted = protractor.promise.defer(); + let numDeleted = 0; + let count; + projectTiles.count().then((num) => { + count = num; + // safely fulfill if there happen to be no projects. + if(count === 0) { + allDeleted.fulfill(); + } + logger.log('LOGGER: projects to delete:', count); + }); + + projectTiles.each((elem, index) => { + logger.log('LOGGER: deleting', index); + let projectTitle = elem.element(by.css('.tile-target span')).getText(); + elem.element(by.css('.dropdown-toggle')).click(); + elem.element(by.linkText('Delete Project')).click(); + deprecatedHelpers.setInputValue('confirmName', projectTitle); + + let deleteModal = element(by.css('.delete-resource-modal')); + wait.forElem(deleteModal); + let deleteButton = deleteModal.element(by.cssContainingText(".modal-dialog .btn", "Delete")); + wait.forClickableElem(deleteButton); + deleteButton.click(); + + // let projectMarkedForDeletionToast = element( + // by.cssContainingText( + // '.toast-pf.alert.alert-success', + // `Project ${projectTitle} was marked for deletion.`)); + // wait.forElem(projectMarkedForDeletionToast); + + numDeleted++; + if(numDeleted >= count) { + logger.log('LOGGER: welp, must be done? (1)', numDeleted, count); + allDeleted.fulfill(numDeleted); + } + }); + return allDeleted.promise; }; diff --git a/test/integration/page-objects/createProject.js b/test/integration/page-objects/createProject.js index b29f5b2094..50e8b61430 100644 --- a/test/integration/page-objects/createProject.js +++ b/test/integration/page-objects/createProject.js @@ -3,7 +3,7 @@ const forms = require('../helpers/forms'); const timing = require('../helpers/timing'); const Page = require('./page').Page; -const ProjectList = require('./projectList').ProjectList; +const ProjectListPage = require('./projectList').ProjectListPage; class CreateProjectPage extends Page { constructor(project, menu) { @@ -17,7 +17,7 @@ class CreateProjectPage extends Page { createProject() { return forms.submitNewProjectForm(this.project).then(() => { return browser.sleep(timing.implicitRedirect).then(() => { - return new ProjectList(); + return new ProjectListPage(); }); }); } diff --git a/test/integration/page-objects/menus.js b/test/integration/page-objects/menus.js index c674c276f3..993353d357 100644 --- a/test/integration/page-objects/menus.js +++ b/test/integration/page-objects/menus.js @@ -25,6 +25,9 @@ let clickNestedMenuItem = function(mainMenuSelector, childMenuSelector) { }; exports.menus = { + clickLogo: () => { + return element(by.id('openshift-logo')).click(); + }, topNav: { // NOTE: this links out of the console clickDocumentation: () => { @@ -141,5 +144,15 @@ exports.menus = { clickMonitoring: () => { return element(by.cssContainingText(selectors.sidePrimary, 'Monitoring')).click(); } + }, + // TODO: once we get a new CatalogPage, migrate this to + // that page-object: + clickViewAllProjects: () => { + return element(by.css('.projects-view-all')).click(); + }, + // ATM seems to be the most sensible place to put this, even + // though it is not an actual menu item. + backToPreviousPage: () => { + return browser.navigate().back(); } }; diff --git a/test/integration/page-objects/projectList.js b/test/integration/page-objects/projectList.js index bbac812f31..d67cfc3cbe 100644 --- a/test/integration/page-objects/projectList.js +++ b/test/integration/page-objects/projectList.js @@ -1,23 +1,94 @@ 'use strict'; const wait = require('../helpers/wait'); +const logger = require('../helpers/logger'); +const inputs = require('../helpers/inputs'); +const winHelper = require('../helpers/window'); +const timing = require('../helpers/timing'); const Page = require('./page').Page; -class ProjectList extends Page { +// these projects should not be deleted +// e2e user should not see them, however. +// const infraProjects = [ +// 'default', +// 'kube-public', +// 'kube-service-catalog', +// 'kube-system', +// 'openshift', +// 'openshift-infra', +// 'openshift-node', +// 'openshift-template-service-broker' +// ]; + +class ProjectTile { + constructor(project) { + this.project = project; + this.tile = element(by.cssContainingText('.list-group-item.project-info', project.name || project.displayName)); + this.tileTargetLink = this.tile.element(by.css('.tile-target')); + this.menuToggle = this.tile.element(by.css('.dropdown-toggle')); + this.menu = this.tile.element(by.css('.dropdown-menu')); + this.viewMembershipButton = this.menu.element(by.cssContainingText('.dropdown-menu li', 'View Membership')); + this.editProjectButton = this.menu.element(by.cssContainingText('.dropdown-menu li', 'Edit Project')); + this.deleteProjectButton = this.menu.element(by.cssContainingText('.dropdown-menu li', 'Delete Project')); + } + click() { + winHelper.scrollToElement(this.tileTargetLink); + return this.tileTargetLink.click(); + } + _toggleMenu() { + winHelper.scrollToElement(this.menuToggle); + return this.menuToggle.click().then(() => { + return browser.sleep(timing.openMenu); + }); + } + clickViewMembership() { + return this._toggleMenu().then(() => { + winHelper.scrollToElement(this.viewMembershipButton); + return this.viewMembershipButton.click(); + }); + } + clickEdit() { + return this._toggleMenu().then(() => { + winHelper.scrollToElement(this.editProjectButton); + return this.editProjectButton.click(); + }); + } + clickDelete() { + return this._toggleMenu().then(() => { + winHelper.scrollToElement(this.deleteProjectButton); + return this.deleteProjectButton.click(); + }); + } + confirmDelete() { + let modal = element(by.css('.modal-dialog')); + wait.forElem(modal); + inputs.setInputValue(by.model('confirmName'), this.project.displayName); + let deleteButton = element(by.cssContainingText(".modal-dialog .btn", "Delete")); + wait.forClickableElem(deleteButton); + deleteButton.click(); + let alert = element(by.cssContainingText('.alert-success', 'marked for deletion')); + wait.forElem(alert); + } +} + +class ProjectListPage extends Page { constructor(project, menu) { super(project, menu); } getUrl() { return 'projects'; } + // @deprecated findProjectTiles() { return element.all(by.css('.list-group-item')); } + // @deprecated findTileBy(projectName) { let elem = element(by.cssContainingText('.tile-target', projectName)); wait.forElem(elem); return elem; } + // @deprecated clickTileBy(projectName) { // This is an implicit redirect, which typically // returns an instance of the new page. It should @@ -25,6 +96,69 @@ class ProjectList extends Page { // method doesn't currently take a full project object return this.findTileBy(projectName).click(); } + clickTile(project) { + let tile = new ProjectTile(project); + return tile.click(); + } + // editProject(project) { + // let tile = new ProjectTile(project); + // return tile.clickEdit(); + // } + viewMembership(project) { + let tile = new ProjectTile(project); + return tile.clickViewMembership(); + } + deleteProject(project) { + let tile = new ProjectTile(project); + return tile.clickDelete().then(() => { + return tile.confirmDelete(); + }); + } + _getProjectNames() { + return element + .all(by.css('.list-group-item .tile-target')) + .map((elm) => { + return elm.getText(); + }).then((texts) => { + return texts; + }); + } + _makeDummyProjects() { + return this._getProjectNames().then((names) => { + return names.map((name) => { + return { + displayName: name + }; + }); + }); + } + deleteAllProjects() { + let allDeleted = protractor.promise.defer(); + let numDeleted = 0; + let count; + + this._makeDummyProjects() + .then((projects) => { + count = projects.length; + if(count === 0) { + allDeleted.fulfill(); + } + logger.log('Deleting', count, 'projects'); + projects.forEach((project) => { + logger.log('Deleting', project.displayName); + let tile = new ProjectTile(project); + tile.clickDelete(); + tile.confirmDelete(); + numDeleted++; + if(numDeleted >= count) { + allDeleted.fulfill(numDeleted); + } + browser.sleep(timing.standardDelay); + }); + + }); + return allDeleted.promise; + } } -exports.ProjectList = ProjectList; +exports.ProjectListPage = ProjectListPage; diff --git a/test/protractor.conf.js b/test/protractor.conf.js index c790187475..7c43df5398 100644 --- a/test/protractor.conf.js +++ b/test/protractor.conf.js @@ -35,8 +35,8 @@ exports.config = { ], // helper tests for local debuggin, these should not need to be run in CI exclude: [ - 'integration/features/user_logs_in.spec.js', - 'integration/features/user_deletes_all_projects.spec.js' + // 'integration/features/user_logs_in.spec.js', + // 'integration/features/user_deletes_all_projects.spec.js' ], // usage: // grunt test-e2e @@ -46,16 +46,16 @@ exports.config = { // set of suites: // grunt test-integration --suite=create-projct,add-template-to-project suites: { - 'create-project': 'integration/features/user_creates_project.spec.js', - 'add-template-to-project': 'integration/features/user_adds_template_to_project.spec.js', + 'create-project': 'integration/features/user_creates_project.spec.js', + 'add-template-to-project': 'integration/features/user_adds_template_to_project.spec.js', 'add-imagestream-to-project': 'integration/features/user_adds_imagestream_to_project.spec.js', - 'create-from-url': 'integration/features/user_creates_from_url.spec.js', - 'delete-all-projects': 'integration/features/user_deletes_all_projects.spec.js', + 'create-from-url': 'integration/features/user_creates_from_url.spec.js', + 'delete-all-projects': 'integration/features/user_deletes_all_projects.spec.js', // simple test to ensure we can get past OAuth - 'login': 'integration/features/user_logs_in.spec.js' + 'login': 'integration/features/user_logs_in.spec.js' }, framework: 'jasmine2', - allScriptsTimeout: 30 * 1000, + allScriptsTimeout: 60 * 1000, getPageTimeout: 30 * 1000, jasmineNodeOpts: { defaultTimeoutInterval: 60 * 1000, From 75fd47bb0c445da28aaf65249224f1228008d805 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Fri, 27 Oct 2017 00:48:48 -0400 Subject: [PATCH 08/10] jshint fix --- test/integration/helpers/project.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/helpers/project.js b/test/integration/helpers/project.js index 6790e5d022..1ae685615d 100644 --- a/test/integration/helpers/project.js +++ b/test/integration/helpers/project.js @@ -1,8 +1,8 @@ 'use strict'; const deprecatedHelpers = require('../helpers'); -// const wait = require('./wait'); -// const logger = require('./logger'); +const wait = require('./wait'); +const logger = require('./logger'); // TODO: factor this out into a proper page object exports.visitCreatePage = () => { From 63742a41ba78688c7201962abeaa75be65d95624 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Thu, 21 Dec 2017 12:34:35 -0500 Subject: [PATCH 09/10] Fix login flow test component with better toggles for Angular syncronization --- test/integration/helpers/common.js | 22 +++++----- test/integration/helpers/timing.js | 8 +++- test/integration/helpers/window.js | 2 +- test/integration/page-objects/login.js | 60 +++++++++++++++++++------- 4 files changed, 63 insertions(+), 29 deletions(-) diff --git a/test/integration/helpers/common.js b/test/integration/helpers/common.js index 577a3299a1..745f987cd7 100644 --- a/test/integration/helpers/common.js +++ b/test/integration/helpers/common.js @@ -8,23 +8,21 @@ const menus = require('../page-objects/menus').menus; const LoginPage = require('../page-objects/login').LoginPage; const ProjectListPage = require('../page-objects/projectList').ProjectListPage; -exports.beforeAll = () => { - windowHelper.setSize(); - logger.log('login'); - // we manually bootstrap angular, so it is suggested to do this - // call up front, however it has not been needed thus far. - // browser.waitForAngular(); +exports.beforeAll = () => { + logger.log('common: beforeAll()'); + logger.log('set window size'); + windowHelper.setSize(1024, 800); + // NOTE: login will toggle Angular sync off & on let loginPage = new LoginPage(); loginPage.login(); - browser.driver.sleep(timing.standardDelay); }; exports.afterAll = () => { - + logger.log('common: afterAll()'); // easier in the afterAll as we have done the login flow by this point. if(environment.isMac) { - logger.log('cleanup: deleting all projects'); + logger.log('cleanup (mac): deleting all projects'); menus.clickLogo(); menus.clickViewAllProjects(); let projectList = new ProjectListPage(); @@ -34,15 +32,15 @@ exports.afterAll = () => { menus.topNav.clickLogout(); browser.sleep(timing.standardDelay); - logger.log('logout'); + logger.log('common: logout'); windowHelper.clearStorage(); }; exports.beforeEach = () => { - + logger.log('common: beforeEach (n/a)'); }; exports.afterEach = () => { - + logger.log('common: afterEach (n/a)'); }; diff --git a/test/integration/helpers/timing.js b/test/integration/helpers/timing.js index 9d3c6d499c..837d22b2d4 100644 --- a/test/integration/helpers/timing.js +++ b/test/integration/helpers/timing.js @@ -21,5 +21,11 @@ module.exports = { // 'Elem did not appear') waitForElement: 1000, maxWaitForElement: 15 * 1000, - pauseBetweenTests: 5 * 1000 + pauseBetweenTests: 5 * 1000, + // longer timeouts for non-angular page interactions + // since we won't have protractor helping us + shortDelay: 1000, + medDelay: 3000, + longDelay: 5000, + xtraLongDelay: 10000 }; diff --git a/test/integration/helpers/window.js b/test/integration/helpers/window.js index b20f64ee70..3ec0c6b3d3 100644 --- a/test/integration/helpers/window.js +++ b/test/integration/helpers/window.js @@ -2,7 +2,7 @@ const timing = require('./timing'); -exports.setSize = (height = 768, width = 1024) => { +exports.setSize = (width = 1024, height = 768) => { browser.driver.manage().window().setSize(width, height); }; diff --git a/test/integration/page-objects/login.js b/test/integration/page-objects/login.js index 1a3c7b4bcf..27c8d5187e 100644 --- a/test/integration/page-objects/login.js +++ b/test/integration/page-objects/login.js @@ -2,28 +2,58 @@ const environment = require('../environment'); const nonAngular = require('../helpers/nonAngular').nonAngular; +const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); + +const webdriver = require('selenium-webdriver'); +const until = webdriver.until; // login page is a non-angular page class LoginPage { constructor() { // don't call super, this is a non-angular page } - // its just convenient to roll everything up - // in one method for this page. + // the login flow is performed against a non-angular page. + // therefore ignoreSyncronization is built in, as well as + // plentiful timeouts to ensure we wait for angular to + // bootstrap after the redirect before protractor picks up + // and continues with the rest of the testing flows. login() { - browser.driver.sleep(0).then(() => { - browser.driver.get(environment.baseUrl); - browser.driver.sleep(1000).then(() => { - nonAngular(() => { - browser.driver.wait(browser.driver.findElement(by.name('username'))).then(() => { - browser.driver.findElement(by.name('username')).sendKeys('e2e-user'); - browser.driver.findElement(by.name('password')).sendKeys('e2e-user'); - browser.driver.findElement(by.css("button[type='submit']")).click(); - }, 1000); - }); - }); - - }); + logger.log('login: Begin login flow'); + logger.log('login: Disabling Angular syncronization'); + browser.ignoreSynchronization = true; + browser.waitForAngularEnabled(false); + + logger.log('login: Visit', environment.baseUrl); + browser.driver.get(environment.baseUrl); + browser.driver.sleep(timing.shortDelay); + + logger.log('login: Locating inputs on page'); + var usernameLocator = by.name('username'); + var passwordLocator = by.name('password'); + var userNameInput = browser.driver.findElement(usernameLocator); + var passwordInput = browser.driver.findElement(passwordLocator); + var submitButton = browser.driver.findElement(by.css("button[type='submit']")); + browser.driver.wait(until.elementLocated(usernameLocator)); + browser.driver.wait(until.elementLocated(passwordLocator)); + browser.driver.sleep(timing.shortDelay); + + logger.log('login: Entering input values'); + userNameInput.sendKeys('e2e-user'); + passwordInput.sendKeys('e2e-user'); + + logger.log('login: Submitting login form'); + submitButton.click(); + + logger.log('login: Waiting for redirect'); + browser.driver.sleep(timing.medDelay); + + logger.log('login: Restoring Angular syncronization'); + browser.ignoreSynchronization = true; + browser.waitForAngularEnabled(false); + browser.driver.sleep(timing.shortDelay); + + logger.log('login: End login flow'); } } From 16dc2817fb137522cbd8ac3b21f2dc05e83325c9 Mon Sep 17 00:00:00 2001 From: benjaminapetersen Date: Tue, 2 Jan 2018 11:31:35 -0500 Subject: [PATCH 10/10] Add more logging for debugging purposes --- .../features/user_creates_project.spec.js | 34 ++++++++++++------- test/integration/page-objects/menus.js | 9 +++-- test/integration/page-objects/projectList.js | 17 ++++++++-- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/test/integration/features/user_creates_project.spec.js b/test/integration/features/user_creates_project.spec.js index 67923ed277..5ef01075ef 100644 --- a/test/integration/features/user_creates_project.spec.js +++ b/test/integration/features/user_creates_project.spec.js @@ -2,6 +2,7 @@ const common = require('../helpers/common'); const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); const projectHelpers = require('../helpers/project'); const matchers = require('../helpers/matchers'); @@ -15,18 +16,22 @@ const menus = require('../page-objects/menus').menus; describe('Authenticated user creates a new project', () => { beforeAll(() => { + logger.log('beforeAll()'); common.beforeAll(); }); beforeEach(() => { + logger.log('beforeEach()'); common.beforeEach(); }); afterEach(() => { + logger.log('afterEach()'); common.afterEach(); }); afterAll(() => { + logger.log('afterAll()'); common.afterAll(); }); @@ -101,27 +106,32 @@ describe('Authenticated user creates a new project', () => { // Documentation link leaves console // Copy Login should also just have its own test - // go to the project list page + // go to the catalog menus.clickLogo(); browser.sleep(timing.navToPage); + // test clicking the tile to view the project menus.clickViewAllProjects(); browser.sleep(timing.navToPage); let projectList2 = new ProjectListPage(); - projectList2.clickTile(project); browser.sleep(timing.navToPage); menus.backToPreviousPage(); - - // projectList2.editProject(project); - // browser.sleep(1000); - // menus.backToPreviousPage(); - - projectList2.viewMembership(project); - browser.sleep(1000); + // test the view membership dropdown + logger.log('view membersip'); + let projectList3 = new ProjectListPage(); + projectList3.viewMembership(project); + browser.sleep(3000); + logger.log('back to previous page'); menus.backToPreviousPage(); - - projectList2.deleteProject(project); - + // test the delete project dropdown + browser.sleep(3000); + logger.log('click logo'); + menus.clickLogo(); + menus.clickViewAllProjects(); + browser.sleep(3000); + logger.log('delete project'); + let projectList4 = new ProjectListPage(); + projectList4.deleteProject(project); }); }); }); diff --git a/test/integration/page-objects/menus.js b/test/integration/page-objects/menus.js index 993353d357..c29c4e1986 100644 --- a/test/integration/page-objects/menus.js +++ b/test/integration/page-objects/menus.js @@ -1,6 +1,7 @@ 'use strict'; const timing = require('../helpers/timing'); +const logger = require('../helpers/logger'); let selectors = { topNav: '.dropdown-menu li a', @@ -148,11 +149,15 @@ exports.menus = { // TODO: once we get a new CatalogPage, migrate this to // that page-object: clickViewAllProjects: () => { - return element(by.css('.projects-view-all')).click(); + logger.log('menus:', 'clickViewAllProjects()'); + browser.sleep(timing.standardDelay); + element(by.css('.projects-view-all')).click(); + browser.sleep(timing.standardDelay); }, // ATM seems to be the most sensible place to put this, even // though it is not an actual menu item. backToPreviousPage: () => { - return browser.navigate().back(); + browser.navigate().back(); + browser.sleep(timing.standardDelay); } }; diff --git a/test/integration/page-objects/projectList.js b/test/integration/page-objects/projectList.js index d67cfc3cbe..b2f632f100 100644 --- a/test/integration/page-objects/projectList.js +++ b/test/integration/page-objects/projectList.js @@ -68,6 +68,7 @@ class ProjectTile { deleteButton.click(); let alert = element(by.cssContainingText('.alert-success', 'marked for deletion')); wait.forElem(alert); + browser.sleep(timing.standardDelay); } } @@ -109,8 +110,11 @@ class ProjectListPage extends Page { return tile.clickViewMembership(); } deleteProject(project) { + logger.log('ProjectList.deleteProject()'); + browser.sleep(1000); let tile = new ProjectTile(project); return tile.clickDelete().then(() => { + logger.log('tile.confirmDelete()'); return tile.confirmDelete(); }); } @@ -123,6 +127,12 @@ class ProjectListPage extends Page { return texts; }); } + // Most PageObjects are passed a project in the constructor. + // The ProjectList does not receive a project, as it lists + // many projects. In order to act on all projects in the + // list, this function loops the list & collects the + // project names manually, then returns a project {object} + // that can be used to generate ProjectTiles _makeDummyProjects() { return this._getProjectNames().then((names) => { return names.map((name) => { @@ -136,21 +146,22 @@ class ProjectListPage extends Page { let allDeleted = protractor.promise.defer(); let numDeleted = 0; let count; - + logger.log('ProjectList:', '.deleteAllProjects()'); this._makeDummyProjects() .then((projects) => { count = projects.length; if(count === 0) { allDeleted.fulfill(); } - logger.log('Deleting', count, 'projects'); + logger.log('ProjectList:', 'Deleting', count, 'project(s)', JSON.stringify(projects)); projects.forEach((project) => { - logger.log('Deleting', project.displayName); + logger.log('ProjectList:', 'Deleting', project.displayName); let tile = new ProjectTile(project); tile.clickDelete(); tile.confirmDelete(); numDeleted++; if(numDeleted >= count) { + logger.log('ProjectList:', 'all projects deleted'); allDeleted.fulfill(numDeleted); } browser.sleep(timing.standardDelay);