From c785c387487a5dfe91d554e59c2e75a746c9b494 Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Thu, 20 Dec 2018 12:01:40 -0500 Subject: [PATCH 1/2] websocket tests initial commit --- awx/ui/test/e2e/tests/test-websockets.js | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 awx/ui/test/e2e/tests/test-websockets.js diff --git a/awx/ui/test/e2e/tests/test-websockets.js b/awx/ui/test/e2e/tests/test-websockets.js new file mode 100644 index 0000000000..1d5934099f --- /dev/null +++ b/awx/ui/test/e2e/tests/test-websockets.js @@ -0,0 +1,44 @@ +import { + getInventorySource, + getJobTemplate, + getProject, + getWorkflowTemplate, + getJob +} from '../fixtures'; + +let data; +const spinny = '//*[contains(@class, "spinny")]'; +const dashboard = '//at-side-nav-item[contains(@name, "DASHBOARD")]'; + +const sparklineIcon = '//div[contains(@class, "SmartStatus-iconContainer")]'; +const running = '//div[@ng-show="job.status === \'running\'"]'; + +module.exports = { + before: (client, done) => { + const resources = [ + getInventorySource('test-websockets'), + getJobTemplate('test-websockets'), + getProject('test-websockets'), + getWorkflowTemplate('test-websockets'), + ]; + Promise.all(resources) + .then(([inventory, job, project, workflow]) => { + data = { inventory, job, project, workflow }; + done(); + }); + client + .login() + .waitForAngular() + .resizeWindow(1200, 1000); + }, + 'Test job template status updates on dashboard': client => { + client.useXpath().findThenClick(dashboard); + getJob('test-websockets-job-template'); // Automatically starts job + client.expect.element(spinny).to.not.be.visible.before(5000); + client.expect.element(sparklineIcon + '[1]' + running) + .to.be.visible.before(10000); + }, + after: client => { + client.end(); + } +}; From 3d1b32c72fc3fc2141c00ffd94e38706a0967baf Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Thu, 20 Dec 2018 12:01:40 -0500 Subject: [PATCH 2/2] websocket tests and fixture updates --- awx/ui/test/e2e/fixtures.js | 99 ++++++++++--------- awx/ui/test/e2e/tests/test-org-permissions.js | 4 +- awx/ui/test/e2e/tests/test-websockets.js | 56 ++++++++--- 3 files changed, 101 insertions(+), 58 deletions(-) diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js index 8130a32133..20ffe3d4c0 100644 --- a/awx/ui/test/e2e/fixtures.js +++ b/awx/ui/test/e2e/fixtures.js @@ -16,7 +16,7 @@ const store = {}; * * @param endpoint - The REST API url suffix. * @param data - Attributes used to create a new endpoint. - * @param [unique=['name']] - An array of keys used to uniquely identify previously + * @param [unique] - An array of keys used to uniquely identify previously * created resources from the endpoint. * */ @@ -52,7 +52,7 @@ const getOrCreate = (endpoint, data, unique = ['name']) => { /* Retrieves an organization, and creates it if it does not exist. * - * @param [namespace=session] - A unique name prefix for the organization. + * @param [namespace] - A unique name prefix for the organization. * */ const getOrganization = (namespace = session) => getOrCreate('/organizations/', { @@ -63,7 +63,7 @@ const getOrganization = (namespace = session) => getOrCreate('/organizations/', /* Retrieves an inventory, and creates it if it does not exist. * Also creates an organization with the same name prefix if needed. * - * @param [namespace=session] - A unique name prefix for the inventory. + * @param [namespace] - A unique name prefix for the inventory. * */ const getInventory = (namespace = session) => getOrganization(namespace) @@ -81,7 +81,7 @@ const getInventory = (namespace = session) => getOrganization(namespace) /* Identical to getInventory except it provides a unique suffix, * "*-inventory-nosource". * - * @param[namespace=session] - A unique name prefix for the inventory. + * @param[namespace] - A unique name prefix for the inventory. */ const getInventoryNoSource = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate('/inventories/', { @@ -98,7 +98,7 @@ const getInventoryNoSource = (namespace = session) => getOrganization(namespace) /* Retrieves a host with the given name prefix, and creates it if it does not exist. * If an inventory does not exist with the same prefix, it is created as well. * - * @param[namespace=session] - A unique name prefix for the host. + * @param[namespace] - A unique name prefix for the host. */ const getHost = (namespace = session) => getInventory(namespace) .then(inventory => getOrCreate('/hosts/', { @@ -112,7 +112,7 @@ const getHost = (namespace = session) => getInventory(namespace) * does not exist. If an organization does not exist with the same prefix, it is * created as well. * - * @param[namespace=session] - A unique name prefix for the host. + * @param[namespace] - A unique name prefix for the host. */ const getInventoryScript = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate('/inventory_scripts/', { @@ -126,7 +126,7 @@ const getInventoryScript = (namespace = session) => getOrganization(namespace) * required dependent inventory and inventory script do not exist, they are also * created. * - * @param[namespace=session] - A unique name prefix for the inventory source. + * @param[namespace] - A unique name prefix for the inventory source. */ const getInventorySource = (namespace = session) => { const promises = [ @@ -146,7 +146,7 @@ const getInventorySource = (namespace = session) => { /* Retrieves an AWS credential, and creates it if it does not exist. * - * @param[namespace=session] - A unique name prefix for the AWS credential. + * @param[namespace] - A unique name prefix for the AWS credential. */ const getAdminAWSCredential = (namespace = session) => { const promises = [ @@ -176,7 +176,7 @@ const getAdminAWSCredential = (namespace = session) => { /* Retrieves a machine credential, and creates it if it does not exist. * - * @param[namespace=session] - A unique name prefix for the machine credential. + * @param[namespace] - A unique name prefix for the machine credential. */ const getAdminMachineCredential = (namespace = session) => { const promises = [ @@ -200,7 +200,7 @@ const getAdminMachineCredential = (namespace = session) => { * If an organization does not exist with the same prefix, it is * created as well. * - * @param[namespace=session] - A unique name prefix for the team. + * @param[namespace] - A unique name prefix for the team. */ const getTeam = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate(`/organizations/${organization.id}/teams/`, { @@ -213,7 +213,7 @@ const getTeam = (namespace = session) => getOrganization(namespace) * name prefix. If an organization does not exist with the same prefix, it is * created as well. * - * @param[namespace=session] - A unique name prefix for the smart inventory. + * @param[namespace] - A unique name prefix for the smart inventory. */ const getSmartInventory = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate('/inventories/', { @@ -228,7 +228,7 @@ const getSmartInventory = (namespace = session) => getOrganization(namespace) * name prefix. If an organization does not exist with the same prefix, it is * created as well. * - * @param[namespace=session] - A unique name prefix for the notification template. + * @param[namespace] - A unique name prefix for the notification template. */ const getNotificationTemplate = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate(`/organizations/${organization.id}/notification_templates/`, { @@ -246,15 +246,21 @@ const getNotificationTemplate = (namespace = session) => getOrganization(namespa * name prefix. If an organization does not exist with the same prefix, it is * created as well. * - * @param[namespace=session] - A unique name prefix for the host. + * @param[namespace] - A unique name prefix for the host. + * @param[scmUrl] - The url of the repository. + * @param[scmType] - The type of scm (git, etc.) */ -const getProject = (namespace = session) => getOrganization(namespace) +const getProject = ( + namespace = session, + scmUrl = 'https://github.com/ansible/ansible-tower-samples', + scmType = 'git' +) => getOrganization(namespace) .then(organization => getOrCreate(`/organizations/${organization.id}/projects/`, { name: `${namespace}-project`, description: namespace, organization: organization.id, - scm_url: 'https://github.com/ansible/ansible-tower-samples', - scm_type: 'git' + scm_url: `${scmUrl}`, + scm_type: `${scmType}` })); const waitForJob = endpoint => { @@ -297,9 +303,15 @@ const getUpdatedProject = (namespace = session) => getProject(namespace) * name prefix. This function also runs getOrCreate for an inventory, * credential, and project with the same prefix. * - * @param[namespace=session] - A unique name prefix for the job template. - */ -const getJobTemplate = (namespace = session) => { + * @param [namespace] - Name prefix for associated dependencies. + * @param [playbook] - Playbook for the job template. + * @param [name] - Unique name prefix for the job template. + * */ +const getJobTemplate = ( + namespace = session, + playbook = 'hello_world.yml', + name = `${namespace}-job-template` +) => { const promises = [ getInventory(namespace), getAdminMachineCredential(namespace), @@ -308,20 +320,26 @@ const getJobTemplate = (namespace = session) => { return Promise.all(promises) .then(([inventory, credential, project]) => getOrCreate('/job_templates/', { - name: `${namespace}-job-template`, + name: `${name}`, description: namespace, inventory: inventory.id, credential: credential.id, project: project.id, - playbook: 'hello_world.yml', + playbook: `${playbook}`, })); }; /* Similar to getJobTemplate, except that it also launches the job. * - * @param[namespace=session] - A unique name prefix for the host. + * @param[namespace] - A unique name prefix for the job and its dependencies. + * @param[playbook] - The playbook file to be run by the job template. + * @param[name] - A unique name for the job template. */ -const getJob = (namespace = session) => getJobTemplate(namespace) +const getJob = ( + namespace = session, + playbook = 'hello_world.yml', + name = `${namespace}-job-template` +) => getJobTemplate(namespace, playbook, name) .then(template => { const launchURL = template.related.launch; return post(launchURL, {}).then(response => { @@ -334,7 +352,7 @@ const getJob = (namespace = session) => getJobTemplate(namespace) * name prefix. If an organization does not exist with the same prefix, it is * created as well. A basic workflow node setup is also created. * - * @param[namespace=session] - A unique name prefix for the workflow template. + * @param[namespace] - A unique name prefix for the workflow template. */ const getWorkflowTemplate = (namespace = session) => { const workflowTemplatePromise = getOrganization(namespace) @@ -380,7 +398,7 @@ const getWorkflowTemplate = (namespace = session) => { * name prefix. If an organization does not exist with the same prefix, * it is also created. * - * @param[namespace=session] - A unique name prefix for the auditor. + * @param[namespace] - A unique name prefix for the auditor. */ const getAuditor = (namespace = session) => getOrganization(namespace) .then(organization => getOrCreate(`/organizations/${organization.id}/users/`, { @@ -398,23 +416,15 @@ const getAuditor = (namespace = session) => getOrganization(namespace) * name prefix. If an organization does not exist with the same prefix, * it is also created. * - * @param[namespace=session] - A unique name prefix for the user. + * @param[namespace] - A unique name prefix for the user's organization. + * @param[username] - A unique name for the user. */ -const getUser = (namespace = session) => getOrganization(namespace) +const getUser = ( + namespace = session, + username = `user-${uuid().substr(0, 8)}` +) => getOrganization(namespace) .then(organization => getOrCreate(`/organizations/${organization.id}/users/`, { - username: `user-${uuid().substr(0, 8)}`, - organization: organization.id, - first_name: 'firstname', - last_name: 'lastname', - email: 'null@ansible.com', - is_superuser: false, - is_system_auditor: false, - password: AWX_E2E_PASSWORD - }, ['username'])); - -const getUserExact = (namespace = session, name) => getOrganization(namespace) - .then(organization => getOrCreate(`/organizations/${organization.id}/users/`, { - username: `${name}`, + username: `${username}`, organization: organization.id, first_name: 'firstname', last_name: 'lastname', @@ -428,7 +438,7 @@ const getUserExact = (namespace = session, name) => getOrganization(namespace) * If a job template or organization does not exist with the same * prefix, they are also created. * - * @param[namespace=session] - A unique name prefix for the template admin. + * @param[namespace] - A unique name prefix for the template admin. */ const getJobTemplateAdmin = (namespace = session) => { const rolePromise = getJobTemplate(namespace) @@ -457,7 +467,7 @@ const getJobTemplateAdmin = (namespace = session) => { * If a job template or organization does not exist with the same * prefix, they are also created. * - * @param[namespace=session] - A unique name prefix for the project admin. + * @param[namespace] - A unique name prefix for the project admin. */ const getProjectAdmin = (namespace = session) => { const rolePromise = getUpdatedProject(namespace) @@ -485,7 +495,7 @@ const getProjectAdmin = (namespace = session) => { /* Retrieves a inventory source schedule, and creates it if it does not exist. * If an inventory source does not exist with the same prefix, it is also created. * - * @param[namespace=session] - A unique name prefix for the schedule. + * @param[namespace] - A unique name prefix for the schedule. */ const getInventorySourceSchedule = (namespace = session) => getInventorySource(namespace) .then(source => getOrCreate(source.related.schedules, { @@ -497,7 +507,7 @@ const getInventorySourceSchedule = (namespace = session) => getInventorySource(n /* Retrieves a job template schedule, and creates it if it does not exist. * If an job template does not exist with the same prefix, it is also created. * - * @param[namespace=session] - A unique name prefix for the schedule. + * @param[namespace] - A unique name prefix for the schedule. */ const getJobTemplateSchedule = (namespace = session) => getJobTemplate(namespace) .then(template => getOrCreate(template.related.schedules, { @@ -529,6 +539,5 @@ module.exports = { getTeam, getUpdatedProject, getUser, - getUserExact, getWorkflowTemplate, }; diff --git a/awx/ui/test/e2e/tests/test-org-permissions.js b/awx/ui/test/e2e/tests/test-org-permissions.js index 88e4f084b1..cb5ce9d0f9 100644 --- a/awx/ui/test/e2e/tests/test-org-permissions.js +++ b/awx/ui/test/e2e/tests/test-org-permissions.js @@ -1,6 +1,6 @@ import { getOrganization, - getUserExact, + getUser, getTeam, } from '../fixtures'; @@ -53,7 +53,7 @@ const readOrgPermissionResults = `//*[@id="permissions_table"]//*[text()="${name module.exports = { before: (client, done) => { const resources = [ - getUserExact(namespace, `${namespace}-user`), + getUser(namespace, `${namespace}-user`), getOrganization(namespace), getTeam(namespace), ]; diff --git a/awx/ui/test/e2e/tests/test-websockets.js b/awx/ui/test/e2e/tests/test-websockets.js index 1d5934099f..16f391c185 100644 --- a/awx/ui/test/e2e/tests/test-websockets.js +++ b/awx/ui/test/e2e/tests/test-websockets.js @@ -1,8 +1,10 @@ +/* Websocket tests. These tests verify that the sparkline (colored box rows which + * display job status) update correctly as the jobs progress. + */ + import { getInventorySource, - getJobTemplate, getProject, - getWorkflowTemplate, getJob } from '../fixtures'; @@ -10,34 +12,66 @@ let data; const spinny = '//*[contains(@class, "spinny")]'; const dashboard = '//at-side-nav-item[contains(@name, "DASHBOARD")]'; +// UI elements 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. const running = '//div[@ng-show="job.status === \'running\'"]'; +const success = '//div[contains(@class, "SmartStatus-iconIndicator--success")]'; +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'), - getJobTemplate('test-websockets'), - getProject('test-websockets'), - getWorkflowTemplate('test-websockets'), + getProject('test-websockets', 'https://github.com/ansible/test-playbooks'), + // launch job templates once before running tests. + getJob('test-websockets', 'debug.yml', 'test-websockets-successful', done), + getJob('test-websockets', 'fail_unless.yml', 'test-websockets-failed', done) ]; + Promise.all(resources) - .then(([inventory, job, project, workflow]) => { - data = { inventory, job, project, workflow }; + .then(([inventory, project, jt1, jt2]) => { + data = { inventory, project, jt1, jt2 }; done(); }); + client .login() .waitForAngular() .resizeWindow(1200, 1000); }, - 'Test job template status updates on dashboard': client => { + + 'Test job template status updates for a successful job on dashboard': client => { client.useXpath().findThenClick(dashboard); - getJob('test-websockets-job-template'); // Automatically starts job + 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(10000); + 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); }, + + 'Test job template status updates for a failed job on dashboard': client => { + client.useXpath().findThenClick(dashboard); + 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); + + // Allow a maximum amount of 30 seconds for the job to complete. + client.expect.element(`${failedJt}${sparklineIcon}[1]${fail}`) + .to.be.present.after(30000); + }, + after: client => { client.end(); }