diff --git a/awx/ui/test/e2e/commands/findThenClick.js b/awx/ui/test/e2e/commands/findThenClick.js index 558ec26944..d7a0fb331e 100644 --- a/awx/ui/test/e2e/commands/findThenClick.js +++ b/awx/ui/test/e2e/commands/findThenClick.js @@ -1,14 +1,18 @@ -const spinny = "//*[contains(@class, 'spinny')]"; - /* Utility function for clicking elements; attempts to scroll to * the element if necessary, and waits for the page to finish loading. * - * @param selector - xpath of the element to click. */ -exports.command = function findThenClick (selector) { + * @param selector - xpath or css selector of the element to click. + * @param [locatoryStrategy='xpath'] - locator strategy used. */ + +exports.command = function findThenClick (selector, locatorStrategy = 'xpath') { this.waitForElementPresent(selector, () => { this.moveToElement(selector, 0, 0, () => { this.click(selector, () => { - this.waitForElementNotVisible(spinny); + if (locatorStrategy === 'css') { + this.waitForElementNotVisible('.spinny'); + } else { + this.waitForElementNotVisible('//*[contains(@class, "spinny")]'); + } }); }); }); diff --git a/awx/ui/test/e2e/commands/navigateTo.js b/awx/ui/test/e2e/commands/navigateTo.js index 4c4509cf61..b79a69338d 100644 --- a/awx/ui/test/e2e/commands/navigateTo.js +++ b/awx/ui/test/e2e/commands/navigateTo.js @@ -5,8 +5,9 @@ exports.command = function navigateTo (url, expectSpinny = true) { this.url(url); if (expectSpinny) { - this.waitForElementVisible(spinny); - this.waitForElementNotVisible(spinny); + this.waitForElementVisible(spinny, () => { + this.waitForElementNotVisible(spinny); + }); } return this; diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js index 20ffe3d4c0..cef013713f 100644 --- a/awx/ui/test/e2e/fixtures.js +++ b/awx/ui/test/e2e/fixtures.js @@ -288,16 +288,18 @@ const waitForJob = endpoint => { }); }; -const getUpdatedProject = (namespace = session) => getProject(namespace) - .then(project => { - const updateURL = project.related.current_update; - - if (updateURL) { - return waitForJob(updateURL).then(() => project); - } - - return project; - }); +const getUpdatedProject = (namespace = session) => { + const promises = [ + getProject(namespace), + ]; + return Promise.all(promises) + .then(([project]) => { + post(`/api/v2/projects/${project.id}/update/`, {}) + .then(update => waitForJob(update.data.url)) + .then(() => { project = getProject(namespace); }); + return project; + }); +}; /* Retrieves a job template, and creates it if it does not exist. * name prefix. This function also runs getOrCreate for an inventory, diff --git a/awx/ui/test/e2e/tests/test-websockets.js b/awx/ui/test/e2e/tests/test-websockets.js index 16f391c185..e3c48e6bbd 100644 --- a/awx/ui/test/e2e/tests/test-websockets.js +++ b/awx/ui/test/e2e/tests/test-websockets.js @@ -1,24 +1,29 @@ -/* Websocket tests. These tests verify that the sparkline (colored box rows which - * display job status) update correctly as the jobs progress. +/* Websocket tests. These tests verify that items like the sparkline (colored box rows which + * display job status) and other status icons update correctly as the jobs progress. */ import { getInventorySource, + getOrganization, getProject, - getJob + getJob, + getUpdatedProject } from '../fixtures'; -let data; -const spinny = '//*[contains(@class, "spinny")]'; -const dashboard = '//at-side-nav-item[contains(@name, "DASHBOARD")]'; +import { + AWX_E2E_URL, + AWX_E2E_TIMEOUT_LONG, + AWX_E2E_TIMEOUT_MEDIUM, +} from '../settings'; -// UI elements for recently run job templates on the dashboard. +let data; + +// Xpath selectors for recently run job templates on the dashboard. const successfulJt = '//a[contains(text(), "test-websockets-successful")]/../..'; const failedJt = '//a[contains(text(), "test-websockets-failed")]/../..'; const sparklineIcon = '//div[contains(@class, "SmartStatus-iconContainer")]'; -// Sparkline icon statuses. -// Running is blinking green, successful is green, fail/error/cancellation is red. +// Xpath selectors for sparkline icon statuses. const running = '//div[@ng-show="job.status === \'running\'"]'; const success = '//div[contains(@class, "SmartStatus-iconIndicator--success")]'; const fail = '//div[contains(@class, "SmartStatus-iconIndicator--failed")]'; @@ -26,18 +31,18 @@ const fail = '//div[contains(@class, "SmartStatus-iconIndicator--failed")]'; module.exports = { before: (client, done) => { - // Jobs only display on the dashboard if they have been run at least once. const resources = [ getInventorySource('test-websockets'), getProject('test-websockets', 'https://github.com/ansible/test-playbooks'), - // launch job templates once before running tests. + getOrganization('test-websockets'), + // launch job templates once before running tests so that they appear on the dashboard. getJob('test-websockets', 'debug.yml', 'test-websockets-successful', done), getJob('test-websockets', 'fail_unless.yml', 'test-websockets-failed', done) ]; Promise.all(resources) - .then(([inventory, project, jt1, jt2]) => { - data = { inventory, project, jt1, jt2 }; + .then(([inventory, project, org, jt1, jt2]) => { + data = { inventory, project, org, jt1, jt2 }; done(); }); @@ -48,28 +53,101 @@ module.exports = { }, 'Test job template status updates for a successful job on dashboard': client => { - client.useXpath().findThenClick(dashboard); + client + .useCss() + .navigateTo(`${AWX_E2E_URL}/#/home`); getJob('test-websockets', 'debug.yml', 'test-websockets-successful'); - client.expect.element(spinny).to.not.be.visible.before(5000); - client.expect.element(`${sparklineIcon}[1]${running}`) - .to.be.visible.before(5000); - // Allow a maximum amount of 30 seconds for the job to complete. - client.expect.element(`${successfulJt}${sparklineIcon}[1]${success}`) - .to.be.present.after(30000); + client.expect.element('.spinny') + .to.not.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${running}`) + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${successfulJt}${sparklineIcon}[1]${success}`) + .to.be.present.before(AWX_E2E_TIMEOUT_LONG); }, 'Test job template status updates for a failed job on dashboard': client => { - client.useXpath().findThenClick(dashboard); + client + .useCss() + .navigateTo(`${AWX_E2E_URL}/#/home`); getJob('test-websockets', 'fail_unless.yml', 'test-websockets-failed'); - client.expect.element(spinny).to.not.be.visible.before(5000); - client.expect.element(`${sparklineIcon}[1]${running}`) - .to.be.visible.before(5000); + client.expect.element('.spinny') + .to.not.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${running}`) + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${failedJt}${sparklineIcon}[1]${fail}`) + .to.be.present.before(AWX_E2E_TIMEOUT_LONG); + }, - // Allow a maximum amount of 30 seconds for the job to complete. - client.expect.element(`${failedJt}${sparklineIcon}[1]${fail}`) - .to.be.present.after(30000); + 'Test projects list blinking icon': client => { + client + .useCss() + .findThenClick('[ui-sref=projects]', 'css') + .clearValue('.SmartSearch-input') + .setValue( + '.SmartSearch-input', + ['name.iexact:"test-websockets-project"', client.Keys.ENTER] + ); + getUpdatedProject('test-websockets'); + + client.expect.element('i.icon-job-running') + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.expect.element('i.icon-job-success') + .to.be.visible.before(AWX_E2E_TIMEOUT_LONG); + }, + + 'Test successful job within an organization view': client => { + client + .useCss() + .navigateTo(`${AWX_E2E_URL}/#/organizations/${data.org.id}/job_templates`) + .clearValue('[ui-view=templatesList] .SmartSearch-input') + .setValue( + '[ui-view=templatesList] .SmartSearch-input', + ['test-websockets-successful', client.Keys.ENTER] + ); + getJob('test-websockets', 'debug.yml', 'test-websockets-successful'); + + client.expect.element('.spinny') + .to.not.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${running}`) + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${success}`) + .to.be.present.before(AWX_E2E_TIMEOUT_LONG); + }, + 'Test failed job within an organization view': client => { + client + .useCss() + .navigateTo(`${AWX_E2E_URL}/#/organizations/${data.org.id}/job_templates`) + .clearValue('[ui-view=templatesList] .SmartSearch-input') + .setValue( + '[ui-view=templatesList] .SmartSearch-input', + ['test-websockets-failed', client.Keys.ENTER] + ); + getJob('test-websockets', 'debug.yml', 'test-websockets-failed'); + + client.expect.element('.spinny') + .to.not.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${running}`) + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.useXpath().expect.element(`${sparklineIcon}[1]${fail}`) + .to.be.present.before(AWX_E2E_TIMEOUT_LONG); + }, + 'Test project blinking icon within an organization view': client => { + client + .useCss() + .navigateTo(`${AWX_E2E_URL}/#/organizations/${data.org.id}/projects`) + .clearValue('.projectsList .SmartSearch-input') + .setValue( + '.projectsList .SmartSearch-input', + ['test-websockets-project', client.Keys.ENTER] + ); + getUpdatedProject('test-websockets'); + + client.expect.element('i.icon-job-running') + .to.be.visible.before(AWX_E2E_TIMEOUT_MEDIUM); + client.expect.element('i.icon-job-success') + .to.be.visible.before(AWX_E2E_TIMEOUT_LONG); }, after: client => {