diff --git a/awx/ui/.eslintignore b/awx/ui/.eslintignore index f290f33893..f1daf7c9ab 100644 --- a/awx/ui/.eslintignore +++ b/awx/ui/.eslintignore @@ -1,6 +1,7 @@ -webpack.*.js Gruntfile.js karma.*.js +webpack.*.js +nightwatch.*.js etc coverage @@ -9,12 +10,10 @@ node_modules po static templates -tests -client/**/*.js -test -!client/lib/components/**/*.js -!client/lib/models/**/*.js -!client/lib/services/**/*.js -!client/features/**/*.js +client/src/**/*.js +client/assets/**/*.js +test/spec/**/*.js + !client/src/app.start.js +!client/src/vendor.js diff --git a/awx/ui/.eslintrc.js b/awx/ui/.eslintrc.js index c624bf962a..c8b8c73bba 100644 --- a/awx/ui/.eslintrc.js +++ b/awx/ui/.eslintrc.js @@ -6,13 +6,19 @@ module.exports = { 'airbnb-base' ], plugins: [ - 'import' + 'import', + 'disable' ], settings: { 'import/resolver': { webpack: { config: path.join(__dirname, 'build/webpack.development.js') } + }, + 'eslint-plugin-disable': { + paths: { + import: ['**/build/*.js'] + } } }, env: { @@ -44,7 +50,9 @@ module.exports = { 'no-plusplus': 'off', 'no-underscore-dangle': 'off', 'no-use-before-define': 'off', + 'no-multiple-empty-lines': ['error', { max: 1 }], 'object-curly-newline': 'off', - 'space-before-function-paren': ['error', 'always'] + 'space-before-function-paren': ['error', 'always'], + 'no-trailing-spaces': ['error'] } }; diff --git a/awx/ui/build/webpack.base.js b/awx/ui/build/webpack.base.js index ae4c3a856d..371a6934aa 100644 --- a/awx/ui/build/webpack.base.js +++ b/awx/ui/build/webpack.base.js @@ -18,12 +18,13 @@ const LANGUAGES_PATH = path.join(CLIENT_PATH, 'languages'); const MODELS_PATH = path.join(LIB_PATH, 'models'); const NODE_MODULES_PATH = path.join(UI_PATH, 'node_modules'); const SERVICES_PATH = path.join(LIB_PATH, 'services'); -const SOURCE_PATH = path.join(CLIENT_PATH, 'src'); +const SRC_PATH = path.join(CLIENT_PATH, 'src'); const STATIC_PATH = path.join(UI_PATH, 'static'); +const TEST_PATH = path.join(UI_PATH, 'test'); const THEME_PATH = path.join(LIB_PATH, 'theme'); -const APP_ENTRY = path.join(SOURCE_PATH, 'app.js'); -const VENDOR_ENTRY = path.join(SOURCE_PATH, 'vendor.js'); +const APP_ENTRY = path.join(SRC_PATH, 'app.js'); +const VENDOR_ENTRY = path.join(SRC_PATH, 'vendor.js'); const INDEX_ENTRY = path.join(CLIENT_PATH, 'index.template.ejs'); const INDEX_OUTPUT = path.join(UI_PATH, 'templates/ui/index.html'); const THEME_ENTRY = path.join(LIB_PATH, 'theme', 'index.less'); @@ -49,7 +50,7 @@ const base = { chunks: false, excludeAssets: name => { const chunkNames = `(${CHUNKS.join('|')})`; - const outputPattern = new RegExp(`${chunkNames}\.[a-f0-9]+\.(js|css)(|\.map)$`, 'i'); + const outputPattern = new RegExp(`${chunkNames}.[a-f0-9]+.(js|css)(|.map)$`, 'i'); return !outputPattern.test(name); } @@ -82,7 +83,7 @@ const base = { }) }, { - test: /\lib\/theme\/index.less$/, + test: /lib\/theme\/index.less$/, use: ExtractTextPlugin.extract({ use: ['css-loader', 'less-loader'] }) @@ -92,7 +93,8 @@ const base = { use: ['ngtemplate-loader', 'html-loader'], include: [ /lib\/components\//, - /features\// + /features\//, + /src\// ] }, { @@ -149,17 +151,17 @@ const base = { context: NODE_MODULES_PATH }, { - from: path.join(SOURCE_PATH, '**/*.partial.html'), + from: path.join(SRC_PATH, '**/*.partial.html'), to: path.join(STATIC_PATH, 'partials/'), - context: SOURCE_PATH + context: SRC_PATH }, { - from: path.join(SOURCE_PATH, 'partials', '*.html'), + from: path.join(SRC_PATH, 'partials', '*.html'), to: STATIC_PATH, - context: SOURCE_PATH + context: SRC_PATH }, { - from: path.join(SOURCE_PATH, '*config.js'), + from: path.join(SRC_PATH, '*config.js'), to: STATIC_PATH, flatten: true } @@ -170,31 +172,34 @@ const base = { filename: INDEX_OUTPUT, inject: false, chunks: CHUNKS, - chunksSortMode: chunk => chunk.names[0] === 'vendor' ? -1 : 1 + chunksSortMode: chunk => (chunk.names[0] === 'vendor' ? -1 : 1) }) ], resolve: { alias: { + '~assets': ASSETS_PATH, + '~components': COMPONENTS_PATH, '~features': FEATURES_PATH, '~models': MODELS_PATH, + '~node_modules': NODE_MODULES_PATH, '~services': SERVICES_PATH, - '~components': COMPONENTS_PATH, + '~src': SRC_PATH, + '~test': TEST_PATH, '~theme': THEME_PATH, - '~modules': NODE_MODULES_PATH, - '~assets': ASSETS_PATH, - 'd3$': '~modules/d3/d3.min.js', - 'codemirror.jsonlint$': '~modules/codemirror/addon/lint/json-lint.js', - 'jquery': '~modules/jquery/dist/jquery.js', - 'jquery-resize$': '~modules/javascript-detect-element-resize/jquery.resize.js', - 'select2$': '~modules/select2/dist/js/select2.full.min.js', - 'js-yaml$': '~modules/js-yaml/dist/js-yaml.min.js', - 'lr-infinite-scroll$': '~modules/lr-infinite-scroll/lrInfiniteScroll.js', - 'angular-tz-extensions$': '~modules/angular-tz-extensions/lib/angular-tz-extensions.js', - 'angular-ui-router$': '~modules/angular-ui-router/release/angular-ui-router.js', - 'angular-ui-router-state-events$': '~modules/angular-ui-router/release/stateEvents.js', - 'ng-toast-provider$': '~modules/ng-toast/src/scripts/provider.js', - 'ng-toast-directives$': '~modules/ng-toast/src/scripts/directives.js', - 'ng-toast$': '~modules/ng-toast/src/scripts/module.js' + '~ui': UI_PATH, + d3$: '~node_modules/d3/d3.min.js', + 'codemirror.jsonlint$': '~node_modules/codemirror/addon/lint/json-lint.js', + jquery: '~node_modules/jquery/dist/jquery.js', + 'jquery-resize$': '~node_modules/javascript-detect-element-resize/jquery.resize.js', + select2$: '~node_modules/select2/dist/js/select2.full.min.js', + 'js-yaml$': '~node_modules/js-yaml/dist/js-yaml.min.js', + 'lr-infinite-scroll$': '~node_modules/lr-infinite-scroll/lrInfiniteScroll.js', + 'angular-tz-extensions$': '~node_modules/angular-tz-extensions/lib/angular-tz-extensions.js', + 'angular-ui-router$': '~node_modules/angular-ui-router/release/angular-ui-router.js', + 'angular-ui-router-state-events$': '~node_modules/angular-ui-router/release/stateEvents.js', + 'ng-toast-provider$': '~node_modules/ng-toast/src/scripts/provider.js', + 'ng-toast-directives$': '~node_modules/ng-toast/src/scripts/directives.js', + 'ng-toast$': '~node_modules/ng-toast/src/scripts/module.js' } } }; diff --git a/awx/ui/build/webpack.development.js b/awx/ui/build/webpack.development.js index a9b2db20d8..56e2d90b51 100644 --- a/awx/ui/build/webpack.development.js +++ b/awx/ui/build/webpack.development.js @@ -1,4 +1,4 @@ -const _ = require('lodash'); +const merge = require('webpack-merge'); const base = require('./webpack.base'); @@ -6,4 +6,4 @@ const development = { devtool: 'source-map' }; -module.exports = _.merge(base, development); +module.exports = merge(base, development); diff --git a/awx/ui/build/webpack.production.js b/awx/ui/build/webpack.production.js index 4c61eef4a4..04ff1cd29b 100644 --- a/awx/ui/build/webpack.production.js +++ b/awx/ui/build/webpack.production.js @@ -1,6 +1,6 @@ const path = require('path'); -const _ = require('lodash'); +const merge = require('webpack-merge'); const webpack = require('webpack'); const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -25,16 +25,14 @@ const production = { filename: INSTALL_RUNNING_OUTPUT, inject: false, chunks: CHUNKS, - chunksSortMode: chunk => chunk.names[0] === 'vendor' ? -1 : 1 + chunksSortMode: chunk => (chunk.names[0] === 'vendor' ? -1 : 1) }), new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('production') + 'process.env': { + NODE_ENV: JSON.stringify('production') } }) ] }; -production.plugins = base.plugins.concat(production.plugins); - -module.exports = _.merge(base, production); +module.exports = merge(base, production); diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index 5030bdf4d4..bdda9363de 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -2,6 +2,8 @@ const path = require('path'); const _ = require('lodash'); const webpack = require('webpack'); +const merge = require('webpack-merge'); +const nodeObjectHash = require('node-object-hash'); const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); @@ -33,9 +35,7 @@ const watch = { new HardSourceWebpackPlugin({ cacheDirectory: 'node_modules/.cache/hard-source/[confighash]', recordsPath: 'node_modules/.cache/hard-source/[confighash]/records.json', - configHash: config => { - return require('node-object-hash')({ sort: false }).hash(config); - }, + configHash: config => nodeObjectHash({ sort: false }).hash(config), environmentHash: { root: process.cwd(), directories: ['node_modules'], @@ -68,7 +68,4 @@ const watch = { } }; -watch.module.rules = development.module.rules.concat(watch.module.rules); -watch.plugins = development.plugins.concat(watch.plugins); - -module.exports = _.merge(development, watch); +module.exports = merge(development, watch); diff --git a/awx/ui/client/src/vendor.js b/awx/ui/client/src/vendor.js index 11c361d314..89e148e7e2 100644 --- a/awx/ui/client/src/vendor.js +++ b/awx/ui/client/src/vendor.js @@ -2,18 +2,20 @@ require('~assets/custom-theme/jquery-ui-1.10.3.custom.min.css'); require('~assets/ansible-bootstrap.min.css'); require('~assets/fontcustom/fontcustom.css'); -require('~modules/components-font-awesome/css/font-awesome.min.css'); -require('~modules/select2/dist/css/select2.css'); -require('~modules/codemirror/lib/codemirror.css'); -require('~modules/codemirror/theme/elegant.css'); -require('~modules/codemirror/addon/lint/lint.css'); -require('~modules/nvd3/build/nv.d3.css'); -require('~modules/ng-toast/dist/ngToast.min.css'); +require('~node_modules/components-font-awesome/css/font-awesome.min.css'); +require('~node_modules/select2/dist/css/select2.css'); +require('~node_modules/codemirror/lib/codemirror.css'); +require('~node_modules/codemirror/theme/elegant.css'); +require('~node_modules/codemirror/addon/lint/lint.css'); +require('~node_modules/nvd3/build/nv.d3.css'); +require('~node_modules/ng-toast/dist/ngToast.min.css'); // jQuery + extensions global.jQuery = require('jquery'); + global.jquery = global.jQuery; global.$ = global.jQuery; + require('jquery-resize'); require('jquery-ui'); require('bootstrap'); diff --git a/awx/ui/client/test/e2e/api.js b/awx/ui/client/test/e2e/api.js deleted file mode 100644 index 464401cb24..0000000000 --- a/awx/ui/client/test/e2e/api.js +++ /dev/null @@ -1,104 +0,0 @@ -import https from 'https'; - -import axios from 'axios'; - -import { - awxURL, - awxUsername, - awxPassword -} from './settings.js'; - - -let authenticated; - -const session = axios.create({ - baseURL: awxURL, - xsrfHeaderName: 'X-CSRFToken', - xsrfCookieName: 'csrftoken', - httpsAgent: new https.Agent({ - rejectUnauthorized: false - }) -}); - - -const endpoint = function(location) { - - if (location.indexOf('/api/v') === 0) { - return location; - } - - if (location.indexOf('://') > 0) { - return location; - } - - return `${awxURL}/api/v2${location}`; -}; - - -const authenticate = function() { - if (authenticated) { - return Promise.resolve(); - } - - let uri = endpoint('/authtoken/'); - - let credentials = { - username: awxUsername, - password: awxPassword - }; - - return session.post(uri, credentials).then(res => { - session.defaults.headers.Authorization = `Token ${res.data.token}`; - authenticated = true; - return res - }); -}; - - -const request = function(method, location, data) { - let uri = endpoint(location); - let action = session[method.toLowerCase()]; - - return authenticate().then(() => action(uri, data)).then(res => { - console.log([ - res.config.method.toUpperCase(), - uri, - res.status, - res.statusText - ].join(' ')); - - return res; - }); -}; - - -const get = function(endpoint, data) { - return request('GET', endpoint, data); -}; - -const options = function(endpoint) { - return request('OPTIONS', endpoint); -}; - -const post = function(endpoint, data) { - return request('POST', endpoint, data); -}; - -const patch = function(endpoint, data) { - return request('PATCH', endpoint, data) -}; - -const put = function(endpoint, data) { - return request('PUT', endpoint, data); -}; - - -module.exports = { - get, - options, - post, - patch, - put, - all: axios.all, - spread: axios.spread -}; diff --git a/awx/ui/client/test/e2e/commands/waitForAngular.js b/awx/ui/client/test/e2e/commands/waitForAngular.js deleted file mode 100644 index ee0e337641..0000000000 --- a/awx/ui/client/test/e2e/commands/waitForAngular.js +++ /dev/null @@ -1,20 +0,0 @@ -exports.command = function(callback) { - let self = this; - this.timeoutsAsyncScript(this.globals.asyncHookTimeout, function() { - this.executeAsync(function(done) { - if(angular && angular.getTestability) { - angular.getTestability(document.body).whenStable(done); - } - else { - done(); - } - }, - [], - function(result) { - if(typeof(callback) === "function") { - callback.call(self, result); - } - }); - }); - return this; -}; diff --git a/awx/ui/client/test/e2e/fixtures.js b/awx/ui/client/test/e2e/fixtures.js deleted file mode 100644 index 0ef3a080f6..0000000000 --- a/awx/ui/client/test/e2e/fixtures.js +++ /dev/null @@ -1,263 +0,0 @@ -import uuid from 'uuid'; - -import { - all, - get, - post, - spread -} from './api.js'; - - -const sid = uuid().substr(0,8); - -let store = {}; - - -const getOrCreate = function(endpoint, data) { - let identifier = Object.keys(data).find(key => ['name', 'username'].includes(key)); - - if (identifier === undefined) { - throw new Error('A unique key value must be provided.'); - } - - let identity = data[identifier]; - - if (store[endpoint] && store[endpoint][identity]) { - return store[endpoint][identity].then(created => created.data); - } - - if (!store[endpoint]) { - store[endpoint] = {}; - } - - let query = { params: { [identifier]: identity } }; - - store[endpoint][identity] = get(endpoint, query).then(res => { - - if (res.data.results.length > 1) { - return Promise.reject(new Error('More than one matching result.')); - } - - if (res.data.results.length === 1) { - return get(res.data.results[0].url); - } - - if (res.data.results.length === 0) { - return post(endpoint, data); - } - - return Promise.reject(new Error(`unexpected response: ${res}`)); - }); - - return store[endpoint][identity].then(created => created.data); -}; - - -const getOrganization = function() { - return getOrCreate('/organizations/', { - name: `e2e-organization-${sid}` - }); -}; - - -const getInventory = function() { - return getOrganization().then(organization => { - return getOrCreate('/inventories/', { - name: `e2e-inventory-${sid}`, - organization: organization.id - }); - }); -}; - - -const getInventoryScript = function() { - return getOrganization().then(organization => { - return getOrCreate('/inventory_scripts/', { - name: `e2e-inventory-script-${sid}`, - organization: organization.id, - script: '#!/usr/bin/env python' - }); - }); -}; - - -const getAdminAWSCredential = function() { - return all([ - get('/me/'), - getOrCreate('/credential_types/', { - name: "Amazon Web Services" - }) - ]) - .then(spread((me, credentialType) => { - let admin = me.data.results[0]; - return getOrCreate('/credentials/', { - name: `e2e-aws-credential-${sid}`, - credential_type: credentialType.id, - user: admin.id, - inputs: { - username: 'admin', - password: 'password', - security_token: 'AAAAAAAAAAAAAAAA' - } - }); - })); -}; - - -const getAdminMachineCredential = function() { - return all([ - get('/me/'), - getOrCreate('/credential_types/', { name: "Machine" }) - ]) - .then(spread((me, credentialType) => { - let admin = me.data.results[0]; - return getOrCreate('/credentials/', { - name: `e2e-machine-credential-${sid}`, - credential_type: credentialType.id, - user: admin.id - }); - })); -}; - - -const getTeam = function() { - return getOrganization().then(organization => { - return getOrCreate('/teams/', { - name: `e2e-team-${sid}`, - organization: organization.id, - }); - }); -}; - - -const getSmartInventory = function() { - return getOrganization().then(organization => { - return getOrCreate('/inventories/', { - name: `e2e-smart-inventory-${sid}`, - organization: organization.id, - host_filter: 'search=localhost', - kind: 'smart' - }); - }); -}; - - -const getNotificationTemplate = function() { - return getOrganization().then(organization => { - return getOrCreate('/notification_templates/', { - name: `e2e-notification-template-${sid}`, - organization: organization.id, - notification_type: 'slack', - notification_configuration: { - token: '54321GFEDCBAABCDEFG12345', - channels: ['awx-e2e'] - } - }); - }); -}; - - -const getProject = function() { - return getOrganization().then(organization => { - return getOrCreate('/projects/', { - name: `e2e-project-${sid}`, - organization: organization.id, - scm_url: 'https://github.com/ansible/ansible-tower-samples', - scm_type: 'git' - }); - }); -}; - - -const waitForJob = function(endpoint) { - const interval = 2000; - const statuses = ['successful', 'failed', 'error', 'canceled']; - - let attempts = 20; - - return new Promise((resolve, reject) => { - (function pollStatus() { - get(endpoint).then(update => { - let completed = statuses.indexOf(update.data.status) > -1; - if (completed) return resolve(); - if (--attempts <= 0) return reject('Retry limit exceeded.'); - setTimeout(pollStatus, interval); - }); - })(); - }); -}; - - -const getUpdatedProject = function() { - return getProject().then(project => { - let updateURL = project.related.current_update; - if (updateURL) { - return waitForJob(updateURL).then(() => project); - } - return project; - }); -}; - - -const getJobTemplate = function() { - return all([ - getInventory(), - getAdminMachineCredential(), - getUpdatedProject() - ]) - .then(spread((inventory, credential, project) => { - return getOrCreate('/job_templates', { - name: `e2e-job-template-${sid}`, - inventory: inventory.id, - credential: credential.id, - project: project.id, - playbook: 'hello_world.yml' - }); - })); -}; - - -const getAuditor = function() { - return getOrganization().then(organization => { - return getOrCreate('/users/', { - organization: organization.id, - username: `e2e-auditor-${sid}`, - first_name: 'auditor', - last_name: 'last', - email: 'null@ansible.com', - is_superuser: false, - is_system_auditor: true, - password: 'password' - }) - }); -}; - - -const getUser = function() { - return getOrCreate('/users/', { - username: `e2e-user-${sid}`, - first_name: `user-${sid}-first`, - last_name: `user-${sid}-last`, - email: `null-${sid}@ansible.com`, - is_superuser: false, - is_system_auditor: false, - password: 'password' - }); -}; - - -module.exports = { - getAdminAWSCredential, - getAdminMachineCredential, - getAuditor, - getInventory, - getInventoryScript, - getJobTemplate, - getNotificationTemplate, - getOrCreate, - getOrganization, - getSmartInventory, - getTeam, - getUpdatedProject, - getUser -}; diff --git a/awx/ui/client/test/e2e/objects/sections/createTableSection.js b/awx/ui/client/test/e2e/objects/sections/createTableSection.js deleted file mode 100644 index 58f134c053..0000000000 --- a/awx/ui/client/test/e2e/objects/sections/createTableSection.js +++ /dev/null @@ -1,63 +0,0 @@ -import dynamicSection from './dynamicSection.js'; - - -const header = { - selector: 'thead', - sections: { - dynamicSection - }, - commands: [{ - findColumnByText(text) { - return this.section.dynamicSection.create({ - name: `column[${text}]`, - locateStrategy: 'xpath', - selector: `.//*[normalize-space(text())='${text}']/ancestor-or-self::th`, - elements: { - sortable: { - locateStrategy: 'xpath', - selector: './/*[contains(@class, "fa-sort")]' - }, - sorted: { - locateStrategy: 'xpath', - selector: './/*[contains(@class, "fa-sort-")]' - }, - } - }); - } - }] -}; - - -const createTableSection = function({ elements, sections, commands }) { - return { - selector: 'table', - sections: { - header, - dynamicSection - }, - commands: [{ - findRowByText(text) { - return this.section.dynamicSection.create({ - elements, - sections, - commands, - name: `row[${text}]`, - locateStrategy: 'xpath', - selector: `.//tbody/tr/td//*[normalize-space(text())='${text}']/ancestor::tr` - }); - }, - waitForRowCount(count) { - let countReached = `tbody tr:nth-of-type(${count})`; - this.waitForElementPresent(countReached); - - let countExceeded = `tbody tr:nth-of-type(${count + 1})`; - this.waitForElementNotPresent(countExceeded); - - return this; - } - }] - }; -}; - - -module.exports = createTableSection; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-custom.js b/awx/ui/client/test/e2e/tests/test-credentials-add-edit-custom.js deleted file mode 100644 index 0606cc7d39..0000000000 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-custom.js +++ /dev/null @@ -1,176 +0,0 @@ -import uuid from 'uuid'; - - -let store = { - credentialType: { - name: `credentialType-${uuid().substr(0,8)}`, - description: "custom cloud credential", - kind: "cloud", - inputs: { - fields: [ - { - id: "project", - label: "Project", - type: "string", - help_text: "Name of your project" - }, - { - id: "token", - label: "Token", - secret: true, - type: "string", - help_text: "help" - }, - { - id: "secret_key_data", - label: "Secret Key", - type: "string", - secret: true, - multiline: true, - help_text: "help", - }, - { - id: "public_key_data", - label: "Public Key", - type: "string", - secret: true, - multiline: true, - help_text: "help", - }, - { - id: "secret_key_unlock", - label: "Private Key Passphrase", - type: "string", - secret: true, - //help_text: "help" - }, - { - id: "color", - label: "Favorite Color", - choices: [ - "", - "red", - "orange", - "yellow", - "green", - "blue", - "indigo", - "violet" - ], - help_text: "help", - }, - ], - required: ['project', 'token'] - }, - injectors: { - env: { - CUSTOM_CREDENTIAL_TOKEN: "{{ token }}" - } - } - } -}; - - -const inputs = store.credentialType.inputs; -const fields = store.credentialType.inputs.fields; -const help = fields.filter(f => f.help_text); -const required = fields.filter(f => inputs.required.indexOf(f.id) > -1); -const strings = fields.filter(f => f.type === undefined || f.type === 'string'); - - -const getObjects = function(client) { - let credentials = client.page.credentials(); - let details = credentials.section.add.section.details; - let type = details.custom(store.credentialType); - return { credentials, details, type }; -}; - - -module.exports = { - before: function(client, done) { - const credentials = client.page.credentials(); - - client.login(); - client.waitForAngular(); - - client.inject([store.credentialType, 'CredentialTypeModel'], (data, model) => { - return new model().http.post(data); - }, - ({ data }) => { - store.credentialType.response = data; - }); - - credentials.section.navigation - .waitForElementVisible('@credentials') - .click('@credentials'); - - credentials - .waitForElementVisible('div.spinny') - .waitForElementNotVisible('div.spinny'); - - credentials.section.list - .waitForElementVisible('@add') - .click('@add'); - - credentials.section.add.section.details - .waitForElementVisible('@save') - .setValue('@name', `cred-${uuid()}`) - .setValue('@type', store.credentialType.name, done); - }, - 'all fields are visible': function(client) { - let { type } = getObjects(client); - fields.map(f => type.expect.element(`@${f.id}`).visible); - }, - 'helplinks open popovers showing expected content': function(client) { - let { type } = getObjects(client); - - help.map(f => { - let group = type.section[f.id]; - group.expect.element('@popover').not.visible; - group.click('@help'); - group.expect.element('@popover').visible; - group.expect.element('@popover').text.to.contain(f.help_text); - group.click('@help'); - }); - - help.map(f => { - let group = type.section[f.id]; - group.expect.element('@popover').not.visible; - }); - }, - 'secret field buttons hide and unhide input': function(client) { - let { type } = getObjects(client); - let secrets = strings.filter(f => f.secret && !f.multiline); - - secrets.map(f => { - let group = type.section[f.id]; - let input = `@${f.id}`; - - group.expect.element('@show').visible; - group.expect.element('@hide').not.present; - - type.setValue(input, 'SECRET'); - type.expect.element(input).text.equal(''); - - group.click('@show'); - group.expect.element('@show').not.present; - group.expect.element('@hide').visible; - type.expect.element(input).value.contain('SECRET'); - - group.click('@hide'); - group.expect.element('@show').visible; - group.expect.element('@hide').not.present; - type.expect.element(input).text.equal(''); - }) - }, - 'required fields show * symbol': function(client) { - let { type } = getObjects(client); - - required.map(f => { - let group = type.section[f.id]; - group.expect.element('@label').text.to.contain('*'); - }); - - client.end(); - } -}; diff --git a/awx/ui/package.json b/awx/ui/package.json index e6893ae15c..2f81d84a18 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -21,12 +21,12 @@ "languages": "grunt nggettext_compile", "build-release": "npm run production", "pretest": "", - "test": "karma start karma.conf.js", + "test": "karma start test/spec/karma.spec.js", "jshint": "grunt jshint:source --no-color", "test:ci": "npm run test -- --single-run --reporter junit,dots --browsers=PhantomJS", - "e2e": "./client/test/e2e/runner.js --config ./client/test/e2e/nightwatch.conf.js", - "component-test": "karma start client/test/unit/karma.conf.js", - "lint": "eslint -c .eslintrc.js .", + "e2e": "./test/e2e/runner.js --config ./test/e2e/nightwatch.conf.js", + "unit": "karma start test/unit/karma.unit.js", + "lint": "eslint .", "dev": "webpack --config build/webpack.development.js --progress", "watch": "webpack-dev-server --config build/webpack.watch.js --progress", "production": "webpack --config build/webpack.production.js" @@ -47,6 +47,7 @@ "eslint-config-airbnb-base": "^12.0.0", "eslint-import-resolver-webpack": "^0.8.3", "eslint-loader": "^1.9.0", + "eslint-plugin-disable": "^0.3.0", "eslint-plugin-import": "^2.7.0", "extract-text-webpack-plugin": "^3.0.0", "grunt": "^1.0.1", @@ -84,8 +85,10 @@ "phantomjs-prebuilt": "^2.1.12", "time-grunt": "^1.4.0", "uglifyjs-webpack-plugin": "^0.4.6", + "uuid": "^3.1.0", "webpack": "^3.0.0", - "webpack-dev-server": "^2.7.1" + "webpack-dev-server": "^2.7.1", + "webpack-merge": "^4.1.0" }, "dependencies": { "angular": "~1.4.14", diff --git a/awx/ui/client/test/e2e/.babelrc b/awx/ui/test/e2e/.babelrc similarity index 100% rename from awx/ui/client/test/e2e/.babelrc rename to awx/ui/test/e2e/.babelrc diff --git a/awx/ui/test/e2e/.eslintrc.js b/awx/ui/test/e2e/.eslintrc.js new file mode 100644 index 0000000000..7ce4f5483c --- /dev/null +++ b/awx/ui/test/e2e/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-unused-expressions': 'off' + } +}; diff --git a/awx/ui/client/test/e2e/README.md b/awx/ui/test/e2e/README.md similarity index 100% rename from awx/ui/client/test/e2e/README.md rename to awx/ui/test/e2e/README.md diff --git a/awx/ui/test/e2e/api.js b/awx/ui/test/e2e/api.js new file mode 100644 index 0000000000..90b29c4355 --- /dev/null +++ b/awx/ui/test/e2e/api.js @@ -0,0 +1,82 @@ +import https from 'https'; + +import axios from 'axios'; + +import { + awxURL, + awxUsername, + awxPassword +} from './settings'; + +let authenticated; + +const session = axios.create({ + baseURL: awxURL, + xsrfHeaderName: 'X-CSRFToken', + xsrfCookieName: 'csrftoken', + httpsAgent: new https.Agent({ + rejectUnauthorized: false + }) +}); + +const getEndpoint = location => { + if (location.indexOf('/api/v') === 0 || location.indexOf('://') > 0) { + return location; + } + + return `${awxURL}/api/v2${location}`; +}; + +const authenticate = () => { + if (authenticated) { + return Promise.resolve(); + } + + const uri = getEndpoint('/authtoken/'); + + const credentials = { + username: awxUsername, + password: awxPassword + }; + + return session.post(uri, credentials).then(res => { + session.defaults.headers.Authorization = `Token ${res.data.token}`; + authenticated = true; + + return res; + }); +}; + +const request = (method, location, data) => { + const uri = getEndpoint(location); + const action = session[method.toLowerCase()]; + + return authenticate() + .then(() => action(uri, data)) + .then(res => { + console.log([ // eslint-disable-line no-console + res.config.method.toUpperCase(), + uri, + res.status, + res.statusText + ].join(' ')); + + return res; + }); +}; + +const get = (endpoint, data) => request('GET', endpoint, data); +const options = endpoint => request('OPTIONS', endpoint); +const post = (endpoint, data) => request('POST', endpoint, data); +const patch = (endpoint, data) => request('PATCH', endpoint, data); +const put = (endpoint, data) => request('PUT', endpoint, data); + +module.exports = { + get, + options, + post, + patch, + put, + all: axios.all, + spread: axios.spread +}; diff --git a/awx/ui/client/test/e2e/cluster/devel-override.yml b/awx/ui/test/e2e/cluster/devel-override.yml similarity index 100% rename from awx/ui/client/test/e2e/cluster/devel-override.yml rename to awx/ui/test/e2e/cluster/devel-override.yml diff --git a/awx/ui/client/test/e2e/cluster/docker-compose.yml b/awx/ui/test/e2e/cluster/docker-compose.yml similarity index 100% rename from awx/ui/client/test/e2e/cluster/docker-compose.yml rename to awx/ui/test/e2e/cluster/docker-compose.yml diff --git a/awx/ui/client/test/e2e/commands/inject.js b/awx/ui/test/e2e/commands/inject.js similarity index 57% rename from awx/ui/client/test/e2e/commands/inject.js rename to awx/ui/test/e2e/commands/inject.js index 50006e57b8..3ecbbcd61c 100644 --- a/awx/ui/client/test/e2e/commands/inject.js +++ b/awx/ui/test/e2e/commands/inject.js @@ -1,5 +1,7 @@ -exports.command = function(deps, script, callback) { - this.executeAsync(`let args = Array.prototype.slice.call(arguments,0); +exports.command = function inject (deps, script, callback) { + this.executeAsync( + `let args = Array.prototype.slice.call(arguments,0); + return function(deps, done) { let injector = angular.element('body').injector(); let loaded = deps.map(d => { @@ -11,11 +13,13 @@ exports.command = function(deps, script, callback) { }); (${script.toString()}).apply(this, loaded).then(done); }.apply(this, args);`, - [deps], - function(result) { - if(typeof(callback) === "function") { - callback.call(this, result.value); + [deps], + function handleResult (result) { + if (typeof callback === 'function') { + callback.call(this, result.value); + } } - }); + ); + return this; }; diff --git a/awx/ui/client/test/e2e/commands/login.js b/awx/ui/test/e2e/commands/login.js similarity index 80% rename from awx/ui/client/test/e2e/commands/login.js rename to awx/ui/test/e2e/commands/login.js index ddf753c496..49acc34553 100644 --- a/awx/ui/client/test/e2e/commands/login.js +++ b/awx/ui/test/e2e/commands/login.js @@ -1,16 +1,13 @@ import { EventEmitter } from 'events'; import { inherits } from 'util'; - -const Login = function() { +function Login () { EventEmitter.call(this); } inherits(Login, EventEmitter); - -Login.prototype.command = function(username, password) { - +Login.prototype.command = function command (username, password) { username = username || this.api.globals.awxUsername; password = password || this.api.globals.awxPassword; @@ -33,16 +30,17 @@ Login.prototype.command = function(username, password) { this.api.elementIdDisplayed(id, ({ value }) => { if (!alertVisible && value) { alertVisible = true; - loginPage.setValue('@username', username) - loginPage.setValue('@password', password) - loginPage.click('@submit') - loginPage.waitForElementVisible('div.spinny') + loginPage.setValue('@username', username); + loginPage.setValue('@password', password); + loginPage.click('@submit'); + loginPage.waitForElementVisible('div.spinny'); loginPage.waitForElementNotVisible('div.spinny'); } - }) - }) + }); + }); + this.emit('complete'); - }) + }); }; module.exports = Login; diff --git a/awx/ui/test/e2e/commands/waitForAngular.js b/awx/ui/test/e2e/commands/waitForAngular.js new file mode 100644 index 0000000000..3f1acf5cd6 --- /dev/null +++ b/awx/ui/test/e2e/commands/waitForAngular.js @@ -0,0 +1,17 @@ +exports.command = function waitForAngular (callback) { + this.timeoutsAsyncScript(this.globals.asyncHookTimeout, () => { + this.executeAsync(done => { + if (angular && angular.getTestability) { + angular.getTestability(document.body).whenStable(done); + } else { + done(); + } + }, [], result => { + if (typeof callback === 'function') { + callback.call(this, result); + } + }); + }); + + return this; +}; diff --git a/awx/ui/test/e2e/fixtures.js b/awx/ui/test/e2e/fixtures.js new file mode 100644 index 0000000000..9f81fd2083 --- /dev/null +++ b/awx/ui/test/e2e/fixtures.js @@ -0,0 +1,235 @@ +import uuid from 'uuid'; + +import { + all, + get, + post, + spread +} from './api'; + +const sid = uuid().substr(0, 8); + +const store = {}; + +const getOrCreate = (endpoint, data) => { + const identifier = Object.keys(data).find(key => ['name', 'username'].includes(key)); + + if (identifier === undefined) { + throw new Error('A unique key value must be provided.'); + } + + const identity = data[identifier]; + + if (store[endpoint] && store[endpoint][identity]) { + return store[endpoint][identity].then(created => created.data); + } + + if (!store[endpoint]) { + store[endpoint] = {}; + } + + const query = { params: { [identifier]: identity } }; + + store[endpoint][identity] = get(endpoint, query) + .then(res => { + if (res.data.results.length > 1) { + return Promise.reject(new Error('More than one matching result.')); + } + + if (res.data.results.length === 1) { + return get(res.data.results[0].url); + } + + if (res.data.results.length === 0) { + return post(endpoint, data); + } + + return Promise.reject(new Error(`unexpected response: ${res}`)); + }); + + return store[endpoint][identity].then(created => created.data); +}; + +const getOrganization = () => getOrCreate('/organizations/', { + name: `e2e-organization-${sid}` +}); + +const getInventory = () => getOrganization() + .then(organization => getOrCreate('/inventories/', { + name: `e2e-inventory-${sid}`, + organization: organization.id + })); + +const getInventoryScript = () => getOrganization() + .then(organization => getOrCreate('/inventory_scripts/', { + name: `e2e-inventory-script-${sid}`, + organization: organization.id, + script: '#!/usr/bin/env python' + })); + +const getAdminAWSCredential = () => { + const promises = [ + get('/me/'), + getOrCreate('/credential_types/', { + name: 'Amazon Web Services' + }) + ]; + + return all(promises) + .then(spread((me, credentialType) => { + const [admin] = me.data.results; + + return getOrCreate('/credentials/', { + name: `e2e-aws-credential-${sid}`, + credential_type: credentialType.id, + user: admin.id, + inputs: { + username: 'admin', + password: 'password', + security_token: 'AAAAAAAAAAAAAAAA' + } + }); + })); +}; + +const getAdminMachineCredential = () => { + const promises = [ + get('/me/'), + getOrCreate('/credential_types/', { name: 'Machine' }) + ]; + + return all(promises) + .then(spread((me, credentialType) => { + const [admin] = me.data.results; + + return getOrCreate('/credentials/', { + name: `e2e-machine-credential-${sid}`, + credential_type: credentialType.id, + user: admin.id + }); + })); +}; + +const getTeam = () => getOrganization() + .then(organization => getOrCreate('/teams/', { + name: `e2e-team-${sid}`, + organization: organization.id, + })); + +const getSmartInventory = () => getOrganization() + .then(organization => getOrCreate('/inventories/', { + name: `e2e-smart-inventory-${sid}`, + organization: organization.id, + host_filter: 'search=localhost', + kind: 'smart' + })); + +const getNotificationTemplate = () => getOrganization() + .then(organization => getOrCreate('/notification_templates/', { + name: `e2e-notification-template-${sid}`, + organization: organization.id, + notification_type: 'slack', + notification_configuration: { + token: '54321GFEDCBAABCDEFG12345', + channels: ['awx-e2e'] + } + })); + +const getProject = () => getOrganization() + .then(organization => getOrCreate('/projects/', { + name: `e2e-project-${sid}`, + organization: organization.id, + scm_url: 'https://github.com/ansible/ansible-tower-samples', + scm_type: 'git' + })); + +const waitForJob = endpoint => { + const interval = 2000; + const statuses = ['successful', 'failed', 'error', 'canceled']; + + let attempts = 20; + + return new Promise((resolve, reject) => { + (function pollStatus () { + get(endpoint).then(update => { + const completed = statuses.indexOf(update.data.status) > -1; + + if (completed) { + return resolve(); + } + + if (--attempts <= 0) { + return reject(new Error('Retry limit exceeded.')); + } + + return setTimeout(pollStatus, interval); + }); + }()); + }); +}; + +const getUpdatedProject = () => getProject() + .then(project => { + const updateURL = project.related.current_update; + + if (updateURL) { + return waitForJob(updateURL).then(() => project); + } + + return project; + }); + +const getJobTemplate = () => { + const promises = [ + getInventory(), + getAdminMachineCredential(), + getUpdatedProject() + ]; + + return all(promises) + .then(spread((inventory, credential, project) => getOrCreate('/job_templates', { + name: `e2e-job-template-${sid}`, + inventory: inventory.id, + credential: credential.id, + project: project.id, + playbook: 'hello_world.yml' + }))); +}; + +const getAuditor = () => getOrganization() + .then(organization => getOrCreate('/users/', { + organization: organization.id, + username: `e2e-auditor-${sid}`, + first_name: 'auditor', + last_name: 'last', + email: 'null@ansible.com', + is_superuser: false, + is_system_auditor: true, + password: 'password' + })); + +const getUser = () => getOrCreate('/users/', { + username: `e2e-user-${sid}`, + first_name: `user-${sid}-first`, + last_name: `user-${sid}-last`, + email: `null-${sid}@ansible.com`, + is_superuser: false, + is_system_auditor: false, + password: 'password' +}); + +module.exports = { + getAdminAWSCredential, + getAdminMachineCredential, + getAuditor, + getInventory, + getInventoryScript, + getJobTemplate, + getNotificationTemplate, + getOrCreate, + getOrganization, + getSmartInventory, + getTeam, + getUpdatedProject, + getUser +}; diff --git a/awx/ui/client/test/e2e/nightwatch.conf.js b/awx/ui/test/e2e/nightwatch.conf.js similarity index 100% rename from awx/ui/client/test/e2e/nightwatch.conf.js rename to awx/ui/test/e2e/nightwatch.conf.js diff --git a/awx/ui/client/test/e2e/objects/activityStream.js b/awx/ui/test/e2e/objects/activityStream.js similarity index 93% rename from awx/ui/client/test/e2e/objects/activityStream.js rename to awx/ui/test/e2e/objects/activityStream.js index b122c02c68..896fa32886 100644 --- a/awx/ui/client/test/e2e/objects/activityStream.js +++ b/awx/ui/test/e2e/objects/activityStream.js @@ -1,6 +1,6 @@ module.exports = { - url() { - return `${this.api.globals.launch_url}/#/activity_stream` + url () { + return `${this.api.globals.launch_url}/#/activity_stream`; }, elements: { title: '.List-titleText', diff --git a/awx/ui/client/test/e2e/objects/credentialTypes.js b/awx/ui/test/e2e/objects/credentialTypes.js similarity index 65% rename from awx/ui/client/test/e2e/objects/credentialTypes.js rename to awx/ui/test/e2e/objects/credentialTypes.js index 4584309543..6c98d1e631 100644 --- a/awx/ui/client/test/e2e/objects/credentialTypes.js +++ b/awx/ui/test/e2e/objects/credentialTypes.js @@ -1,11 +1,10 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import search from './sections/search.js'; -import pagination from './sections/pagination.js'; - +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import search from './sections/search'; +import pagination from './sections/pagination'; const addEditPanel = { selector: 'div[ui-view="form"]', @@ -16,17 +15,16 @@ const addEditPanel = { details: createFormSection({ selector: '#credential_type_form', labels: { - name: "Name", - description: "Description", - inputConfiguration: "Input Configuration", - injectorConfiguration: "Injector Configuration" + name: 'Name', + description: 'Description', + inputConfiguration: 'Input Configuration', + injectorConfiguration: 'Injector Configuration' }, strategy: 'legacy' }) } }; - const listPanel = { selector: 'div[ui-view="list"]', elements: { @@ -50,10 +48,9 @@ const listPanel = { } }; - module.exports = { - url() { - return `${this.api.globals.launch_url}/#/credential_types` + url () { + return `${this.api.globals.launch_url}/#/credential_types`; }, sections: { header, diff --git a/awx/ui/client/test/e2e/objects/credentials.js b/awx/ui/test/e2e/objects/credentials.js similarity index 56% rename from awx/ui/client/test/e2e/objects/credentials.js rename to awx/ui/test/e2e/objects/credentials.js index 7cb0a6de78..23fd718c44 100644 --- a/awx/ui/client/test/e2e/objects/credentials.js +++ b/awx/ui/test/e2e/objects/credentials.js @@ -1,153 +1,140 @@ import _ from 'lodash'; -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import dynamicSection from './sections/dynamicSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; - +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import dynamicSection from './sections/dynamicSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const common = createFormSection({ selector: 'form', labels: { - name: "Name", - description: "Description", - organization: "Organization", - type: "Credential type" + name: 'Name', + description: 'Description', + organization: 'Organization', + type: 'Credential type' } }); - const machine = createFormSection({ selector: '.at-InputGroup-inset', labels: { - username: "Username", - password: "Password", - sshKeyData: "SSH Private Key", - sshKeyUnlock: "Private Key Passphrase", - becomeMethod: "Privilege Escalation Method", - becomeUsername: "Privilege Escalation Username", - becomePassword: "Privilege Escalation Password" + username: 'Username', + password: 'Password', + sshKeyData: 'SSH Private Key', + sshKeyUnlock: 'Private Key Passphrase', + becomeMethod: 'Privilege Escalation Method', + becomeUsername: 'Privilege Escalation Username', + becomePassword: 'Privilege Escalation Password' } }); - const vault = createFormSection({ selector: '.at-InputGroup-inset', labels: { - vaultPassword: "Vault Password", + vaultPassword: 'Vault Password', } }); - const scm = createFormSection({ selector: '.at-InputGroup-inset', labels: { - username: "Username", - password: "Password", - sshKeyData: "SCM Private Key", - sshKeyUnlock: "Private Key Passphrase", + username: 'Username', + password: 'Password', + sshKeyData: 'SCM Private Key', + sshKeyUnlock: 'Private Key Passphrase', } }); - const aws = createFormSection({ selector: '.at-InputGroup-inset', labels: { - accessKey: "Access Key", - secretKey: "Secret Key", - securityToken: "STS Token", + accessKey: 'Access Key', + secretKey: 'Secret Key', + securityToken: 'STS Token', } }); - const gce = createFormSection({ selector: '.at-InputGroup-inset', labels: { - email: "Service Account Email Address", - project: "Project", - sshKeyData: "RSA Private Key", + email: 'Service Account Email Address', + project: 'Project', + sshKeyData: 'RSA Private Key', } }); - const vmware = createFormSection({ selector: '.at-InputGroup-inset', labels: { - host: "VCenter Host", - username: "Username", - password: "Password", + host: 'VCenter Host', + username: 'Username', + password: 'Password', } }); - const azureClassic = createFormSection({ selector: '.at-InputGroup-inset', labels: { - subscription: "Subscription ID", - sshKeyData: "Management Certificate", + subscription: 'Subscription ID', + sshKeyData: 'Management Certificate', } }); - const azure = createFormSection({ selector: '.at-InputGroup-inset', labels: { - subscription: "Subscription ID", - username: "Username", - password: "Password", - client: "Client ID", - secret: "Client Secret", - tenant: "Tenant ID", + subscription: 'Subscription ID', + username: 'Username', + password: 'Password', + client: 'Client ID', + secret: 'Client Secret', + tenant: 'Tenant ID', } }); - const openStack = createFormSection({ selector: '.at-InputGroup-inset', labels: { - username: "Username", - password: "Password (API Key)", - host: "Host (Authentication URL)", - project: "Project (Tenant Name)", - domain: "Domain Name", + username: 'Username', + password: 'Password (API Key)', + host: 'Host (Authentication URL)', + project: 'Project (Tenant Name)', + domain: 'Domain Name', } }); - const rackspace = createFormSection({ selector: '.at-InputGroup-inset', labels: { - username: "Username", - password: "Password", + username: 'Username', + password: 'Password', } }); - const cloudForms = createFormSection({ selector: '.at-InputGroup-inset', labels: { - host: "Cloudforms URL", - username: "Username", - password: "Password", + host: 'Cloudforms URL', + username: 'Username', + password: 'Password', } }); - const network = createFormSection({ selector: '.at-InputGroup-inset', labels: { - sshKeyData: "SSH Private Key", - sshKeyUnlock: "Private Key Passphrase", - username: "Username", - password: "Password", - authorizePassword: "Authorize Password", + sshKeyData: 'SSH Private Key', + sshKeyUnlock: 'Private Key Passphrase', + username: 'Username', + password: 'Password', + authorizePassword: 'Authorize Password', } }); @@ -156,26 +143,23 @@ network.elements.authorize = { selector: '//input[../p/text() = "Authorize"]' }; - const sat6 = createFormSection({ selector: '.at-InputGroup-inset', labels: { - host: "Satellite 6 URL", - username: "Username", - password: "Password", + host: 'Satellite 6 URL', + username: 'Username', + password: 'Password', } }); - const insights = createFormSection({ selector: '.at-InputGroup-inset', labels: { - username: "Username", - password: "Password", + username: 'Username', + password: 'Password', }, }); - const details = _.merge({}, common, { elements: { cancel: '.btn[type="cancel"]', @@ -199,17 +183,17 @@ const details = _.merge({}, common, { vmware }, commands: [{ - custom({ name, inputs }) { - let labels = {}; - inputs.fields.map(f => labels[f.id] = f.label); + custom ({ name, inputs }) { + const labels = {}; + inputs.fields.forEach(f => { labels[f.id] = f.label; }); - let selector = '.at-InputGroup-inset'; - let generated = createFormSection({ selector, labels }); + const selector = '.at-InputGroup-inset'; + const generated = createFormSection({ selector, labels }); - let params = _.merge({ name }, generated); + const params = _.merge({ name }, generated); return this.section.dynamicSection.create(params); }, - clear() { + clear () { this.clearValue('@name'); this.clearValue('@organization'); this.clearValue('@description'); @@ -217,7 +201,7 @@ const details = _.merge({}, common, { this.waitForElementNotVisible('.at-InputGroup-inset'); return this; }, - clearAndSelectType(type) { + clearAndSelectType (type) { this.clear(); this.setValue('@type', type); this.waitForElementVisible('.at-InputGroup-inset'); @@ -226,10 +210,9 @@ const details = _.merge({}, common, { }] }); - module.exports = { - url() { - return `${this.api.globals.launch_url}/#/credentials` + url () { + return `${this.api.globals.launch_url}/#/credentials`; }, sections: { header, diff --git a/awx/ui/client/test/e2e/objects/inventories.js b/awx/ui/test/e2e/objects/inventories.js similarity index 84% rename from awx/ui/client/test/e2e/objects/inventories.js rename to awx/ui/test/e2e/objects/inventories.js index bbef08ff2e..5a87c039f3 100644 --- a/awx/ui/client/test/e2e/objects/inventories.js +++ b/awx/ui/test/e2e/objects/inventories.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const standardInvDetails = createFormSection({ selector: 'form', @@ -36,7 +36,7 @@ const smartInvDetails = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/inventories`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/inventoryScripts.js b/awx/ui/test/e2e/objects/inventoryScripts.js similarity index 76% rename from awx/ui/client/test/e2e/objects/inventoryScripts.js rename to awx/ui/test/e2e/objects/inventoryScripts.js index 3a9da46f00..4bea0c55e9 100644 --- a/awx/ui/client/test/e2e/objects/inventoryScripts.js +++ b/awx/ui/test/e2e/objects/inventoryScripts.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -21,7 +21,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/inventory_scripts`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/login.js b/awx/ui/test/e2e/objects/login.js similarity index 73% rename from awx/ui/client/test/e2e/objects/login.js rename to awx/ui/test/e2e/objects/login.js index 7613ee855b..7fa8e5d8d4 100644 --- a/awx/ui/client/test/e2e/objects/login.js +++ b/awx/ui/test/e2e/objects/login.js @@ -1,6 +1,6 @@ module.exports = { - url() { - return `${this.api.globals.launch_url}/#/login` + url () { + return `${this.api.globals.launch_url}/#/login`; }, elements: { username: '#login-username', diff --git a/awx/ui/client/test/e2e/objects/notificationTemplates.js b/awx/ui/test/e2e/objects/notificationTemplates.js similarity index 79% rename from awx/ui/client/test/e2e/objects/notificationTemplates.js rename to awx/ui/test/e2e/objects/notificationTemplates.js index 72ebd53045..f98fb4bd2e 100644 --- a/awx/ui/client/test/e2e/objects/notificationTemplates.js +++ b/awx/ui/test/e2e/objects/notificationTemplates.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -26,7 +26,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/notification_templates`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/organizations.js b/awx/ui/test/e2e/objects/organizations.js similarity index 77% rename from awx/ui/client/test/e2e/objects/organizations.js rename to awx/ui/test/e2e/objects/organizations.js index b17c1d55f1..d1927affbd 100644 --- a/awx/ui/client/test/e2e/objects/organizations.js +++ b/awx/ui/test/e2e/objects/organizations.js @@ -1,11 +1,11 @@ -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -19,7 +19,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/organizations`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/projects.js b/awx/ui/test/e2e/objects/projects.js similarity index 78% rename from awx/ui/client/test/e2e/objects/projects.js rename to awx/ui/test/e2e/objects/projects.js index e1956a8e83..75194e727b 100644 --- a/awx/ui/client/test/e2e/objects/projects.js +++ b/awx/ui/test/e2e/objects/projects.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -22,7 +22,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/projects`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/sections/actions.js b/awx/ui/test/e2e/objects/sections/actions.js similarity index 99% rename from awx/ui/client/test/e2e/objects/sections/actions.js rename to awx/ui/test/e2e/objects/sections/actions.js index d99b4521c9..afa182e9b0 100644 --- a/awx/ui/client/test/e2e/objects/sections/actions.js +++ b/awx/ui/test/e2e/objects/sections/actions.js @@ -12,5 +12,4 @@ const actions = { } }; - module.exports = actions; diff --git a/awx/ui/client/test/e2e/objects/sections/breadcrumb.js b/awx/ui/test/e2e/objects/sections/breadcrumb.js similarity index 100% rename from awx/ui/client/test/e2e/objects/sections/breadcrumb.js rename to awx/ui/test/e2e/objects/sections/breadcrumb.js diff --git a/awx/ui/client/test/e2e/objects/sections/createFormSection.js b/awx/ui/test/e2e/objects/sections/createFormSection.js similarity index 69% rename from awx/ui/client/test/e2e/objects/sections/createFormSection.js rename to awx/ui/test/e2e/objects/sections/createFormSection.js index 1bdeeb0c54..3a8ebb810b 100644 --- a/awx/ui/client/test/e2e/objects/sections/createFormSection.js +++ b/awx/ui/test/e2e/objects/sections/createFormSection.js @@ -1,10 +1,8 @@ import { merge } from 'lodash'; - const translated = "translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"; const normalized = `normalize-space(${translated})`; - const inputContainerElements = { lookup: 'button > i[class="fa fa-search"]', error: '.at-InputMessage--rejected', @@ -39,7 +37,6 @@ const inputContainerElements = { } }; - const legacyContainerElements = merge({}, inputContainerElements, { prompt: { locateStrategy: 'xpath', @@ -49,22 +46,21 @@ const legacyContainerElements = merge({}, inputContainerElements, { popover: ':root div[id^="popover"]', }); - -const generateInputSelectors = function(label, containerElements) { +const generateInputSelectors = (label, containerElements) => { // descend until span with matching text attribute is encountered - let span = `.//span[text()="${label}"]`; + const span = `.//span[text()="${label}"]`; // recurse upward until div with form-group in class attribute is encountered - let container = `${span}/ancestor::div[contains(@class, 'form-group')]`; + const container = `${span}/ancestor::div[contains(@class, 'form-group')]`; // descend until element with form-control in class attribute is encountered - let input = `${container}//*[contains(@class, 'form-control')]`; + const input = `${container}//*[contains(@class, 'form-control')]`; - let inputContainer = { + const inputContainer = { locateStrategy: 'xpath', selector: container, elements: containerElements }; - let inputElement = { + const inputElement = { locateStrategy: 'xpath', selector: input }; @@ -72,15 +68,14 @@ const generateInputSelectors = function(label, containerElements) { return { inputElement, inputContainer }; }; +function checkAllFieldsDisabled () { + const client = this.client.api; -const checkAllFieldsDisabled = function() { - let client = this.client.api; - - let selectors = this.props.formElementSelectors ? this.props.formElementSelectors : [ + const selectors = this.props.formElementSelectors ? this.props.formElementSelectors : [ '.at-Input' ]; - selectors.forEach(function(selector) { + selectors.forEach(selector => { client.elements('css selector', selector, inputs => { inputs.value.map(o => o.ELEMENT).forEach(id => { client.elementIdAttribute(id, 'disabled', ({ value }) => { @@ -89,37 +84,37 @@ const checkAllFieldsDisabled = function() { }); }); }); -}; - +} const generatorOptions = { default: inputContainerElements, legacy: legacyContainerElements }; +const createFormSection = ({ selector, labels, strategy, props }) => { + const options = generatorOptions[strategy || 'default']; -const createFormSection = function({ selector, labels, strategy, props }) { - let options = generatorOptions[strategy || 'default']; - - let formSection = { + const formSection = { + props, selector, sections: {}, elements: {}, - commands: [{ - checkAllFieldsDisabled: checkAllFieldsDisabled - }], - props: props + commands: [{ checkAllFieldsDisabled }] }; - for (let key in labels) { - let label = labels[key]; - - let { inputElement, inputContainer } = generateInputSelectors(label, options); - - formSection.elements[key] = inputElement; - formSection.sections[key] = inputContainer; + if (!labels) { + return formSection; } + Object.keys(labels) + .forEach(key => { + const label = labels[key]; + const { inputElement, inputContainer } = generateInputSelectors(label, options); + + formSection.elements[key] = inputElement; + formSection.sections[key] = inputContainer; + }); + return formSection; }; diff --git a/awx/ui/test/e2e/objects/sections/createTableSection.js b/awx/ui/test/e2e/objects/sections/createTableSection.js new file mode 100644 index 0000000000..54dfd67eba --- /dev/null +++ b/awx/ui/test/e2e/objects/sections/createTableSection.js @@ -0,0 +1,57 @@ +import dynamicSection from './dynamicSection'; + +const header = { + selector: 'thead', + sections: { + dynamicSection + }, + commands: [{ + findColumnByText (text) { + return this.section.dynamicSection.create({ + name: `column[${text}]`, + locateStrategy: 'xpath', + selector: `.//*[normalize-space(text())='${text}']/ancestor-or-self::th`, + elements: { + sortable: { + locateStrategy: 'xpath', + selector: './/*[contains(@class, "fa-sort")]' + }, + sorted: { + locateStrategy: 'xpath', + selector: './/*[contains(@class, "fa-sort-")]' + }, + } + }); + } + }] +}; + +module.exports = ({ elements, sections, commands }) => ({ + selector: 'table', + sections: { + header, + dynamicSection + }, + commands: [{ + findRowByText (text) { + return this.section.dynamicSection.create({ + elements, + sections, + commands, + name: `row[${text}]`, + locateStrategy: 'xpath', + selector: `.//tbody/tr/td//*[normalize-space(text())='${text}']/ancestor::tr` + }); + }, + waitForRowCount (count) { + const countReached = `tbody tr:nth-of-type(${count})`; + this.waitForElementPresent(countReached); + + const countExceeded = `tbody tr:nth-of-type(${count + 1})`; + this.waitForElementNotPresent(countExceeded); + + return this; + } + }] +}); + diff --git a/awx/ui/client/test/e2e/objects/sections/dynamicSection.js b/awx/ui/test/e2e/objects/sections/dynamicSection.js similarity index 70% rename from awx/ui/client/test/e2e/objects/sections/dynamicSection.js rename to awx/ui/test/e2e/objects/sections/dynamicSection.js index d2cc472f2a..3478066318 100644 --- a/awx/ui/client/test/e2e/objects/sections/dynamicSection.js +++ b/awx/ui/test/e2e/objects/sections/dynamicSection.js @@ -1,10 +1,10 @@ const dynamicSection = { selector: '.', commands: [{ - create({ name, locateStrategy, selector, elements, sections, commands }) { - let Section = this.constructor; + create ({ name, locateStrategy, selector, elements, sections, commands }) { + const Section = this.constructor; - let options = Object.assign(Object.create(this), { + const options = Object.assign(Object.create(this), { name, locateStrategy, elements, diff --git a/awx/ui/client/test/e2e/objects/sections/header.js b/awx/ui/test/e2e/objects/sections/header.js similarity index 99% rename from awx/ui/client/test/e2e/objects/sections/header.js rename to awx/ui/test/e2e/objects/sections/header.js index f0ecb73e2a..ceea0f004b 100644 --- a/awx/ui/client/test/e2e/objects/sections/header.js +++ b/awx/ui/test/e2e/objects/sections/header.js @@ -8,5 +8,4 @@ const header = { } }; - module.exports = header; diff --git a/awx/ui/client/test/e2e/objects/sections/lookupModal.js b/awx/ui/test/e2e/objects/sections/lookupModal.js similarity index 79% rename from awx/ui/client/test/e2e/objects/sections/lookupModal.js rename to awx/ui/test/e2e/objects/sections/lookupModal.js index e6ac0f9a64..15c0b3bee8 100644 --- a/awx/ui/client/test/e2e/objects/sections/lookupModal.js +++ b/awx/ui/test/e2e/objects/sections/lookupModal.js @@ -1,7 +1,6 @@ -import createTableSection from './createTableSection.js'; -import pagination from './pagination.js'; -import search from './search.js'; - +import createTableSection from './createTableSection'; +import pagination from './pagination'; +import search from './search'; const lookupModal = { selector: '#form-modal', diff --git a/awx/ui/client/test/e2e/objects/sections/navigation.js b/awx/ui/test/e2e/objects/sections/navigation.js similarity index 99% rename from awx/ui/client/test/e2e/objects/sections/navigation.js rename to awx/ui/test/e2e/objects/sections/navigation.js index fd978a0f94..62f3359202 100644 --- a/awx/ui/client/test/e2e/objects/sections/navigation.js +++ b/awx/ui/test/e2e/objects/sections/navigation.js @@ -21,5 +21,4 @@ const navigation = { } }; - module.exports = navigation; diff --git a/awx/ui/client/test/e2e/objects/sections/pagination.js b/awx/ui/test/e2e/objects/sections/pagination.js similarity index 100% rename from awx/ui/client/test/e2e/objects/sections/pagination.js rename to awx/ui/test/e2e/objects/sections/pagination.js diff --git a/awx/ui/client/test/e2e/objects/sections/permissions.js b/awx/ui/test/e2e/objects/sections/permissions.js similarity index 79% rename from awx/ui/client/test/e2e/objects/sections/permissions.js rename to awx/ui/test/e2e/objects/sections/permissions.js index 359206c0d9..66a9d467fe 100644 --- a/awx/ui/client/test/e2e/objects/sections/permissions.js +++ b/awx/ui/test/e2e/objects/sections/permissions.js @@ -1,8 +1,7 @@ -import actions from './actions.js'; -import createTableSection from './createTableSection.js'; -import pagination from './pagination.js'; -import search from './search.js'; - +import actions from './actions'; +import createTableSection from './createTableSection'; +import pagination from './pagination'; +import search from './search'; const permissions = { selector: 'div[ui-view="related"]', @@ -26,5 +25,4 @@ const permissions = { } }; - module.exports = permissions; diff --git a/awx/ui/client/test/e2e/objects/sections/search.js b/awx/ui/test/e2e/objects/sections/search.js similarity index 99% rename from awx/ui/client/test/e2e/objects/sections/search.js rename to awx/ui/test/e2e/objects/sections/search.js index f88d1f282b..2fedb8fa37 100644 --- a/awx/ui/client/test/e2e/objects/sections/search.js +++ b/awx/ui/test/e2e/objects/sections/search.js @@ -8,5 +8,4 @@ const search = { } }; - module.exports = search; diff --git a/awx/ui/client/test/e2e/objects/teams.js b/awx/ui/test/e2e/objects/teams.js similarity index 76% rename from awx/ui/client/test/e2e/objects/teams.js rename to awx/ui/test/e2e/objects/teams.js index 245ee2eba2..bc1a4bd940 100644 --- a/awx/ui/client/test/e2e/objects/teams.js +++ b/awx/ui/test/e2e/objects/teams.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -20,7 +20,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/teams`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/templates.js b/awx/ui/test/e2e/objects/templates.js similarity index 82% rename from awx/ui/client/test/e2e/objects/templates.js rename to awx/ui/test/e2e/objects/templates.js index 4e3b69cc4f..b200315427 100644 --- a/awx/ui/client/test/e2e/objects/templates.js +++ b/awx/ui/test/e2e/objects/templates.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -24,7 +24,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/templates`; }, sections: { diff --git a/awx/ui/client/test/e2e/objects/users.js b/awx/ui/test/e2e/objects/users.js similarity index 76% rename from awx/ui/client/test/e2e/objects/users.js rename to awx/ui/test/e2e/objects/users.js index 9d8cdc0eeb..784d7f57fd 100644 --- a/awx/ui/client/test/e2e/objects/users.js +++ b/awx/ui/test/e2e/objects/users.js @@ -1,13 +1,13 @@ -import actions from './sections/actions.js'; -import breadcrumb from './sections/breadcrumb.js'; -import createFormSection from './sections/createFormSection.js'; -import createTableSection from './sections/createTableSection.js'; -import header from './sections/header.js'; -import lookupModal from './sections/lookupModal.js'; -import navigation from './sections/navigation.js'; -import pagination from './sections/pagination.js'; -import permissions from './sections/permissions.js'; -import search from './sections/search.js'; +import actions from './sections/actions'; +import breadcrumb from './sections/breadcrumb'; +import createFormSection from './sections/createFormSection'; +import createTableSection from './sections/createTableSection'; +import header from './sections/header'; +import lookupModal from './sections/lookupModal'; +import navigation from './sections/navigation'; +import pagination from './sections/pagination'; +import permissions from './sections/permissions'; +import search from './sections/search'; const details = createFormSection({ selector: 'form', @@ -20,7 +20,7 @@ const details = createFormSection({ }); module.exports = { - url() { + url () { return `${this.api.globals.launch_url}/#/users`; }, sections: { diff --git a/awx/ui/client/test/e2e/runner.js b/awx/ui/test/e2e/runner.js similarity index 100% rename from awx/ui/client/test/e2e/runner.js rename to awx/ui/test/e2e/runner.js diff --git a/awx/ui/client/test/e2e/settings.js b/awx/ui/test/e2e/settings.js similarity index 99% rename from awx/ui/client/test/e2e/settings.js rename to awx/ui/test/e2e/settings.js index 0a5c892461..f3bbf1f4b2 100644 --- a/awx/ui/client/test/e2e/settings.js +++ b/awx/ui/test/e2e/settings.js @@ -10,7 +10,6 @@ const AWX_E2E_TIMEOUT_LONG = process.env.AWX_E2E_TIMEOUT_LONG || 10000; const AWX_E2E_TIMEOUT_ASYNC = process.env.AWX_E2E_TIMEOUT_ASYNC || 30000; const AWX_E2E_WORKERS = process.env.AWX_E2E_WORKERS || 0; - module.exports = { awxURL: AWX_E2E_URL, awxUsername: AWX_E2E_USERNAME, diff --git a/awx/ui/client/test/e2e/tests/test-auditor-read-only-forms.js b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js similarity index 61% rename from awx/ui/client/test/e2e/tests/test-auditor-read-only-forms.js rename to awx/ui/test/e2e/tests/test-auditor-read-only-forms.js index e0646fe718..f31d33e173 100644 --- a/awx/ui/client/test/e2e/tests/test-auditor-read-only-forms.js +++ b/awx/ui/test/e2e/tests/test-auditor-read-only-forms.js @@ -1,4 +1,4 @@ -import { all } from '../api.js'; +import { all } from '../api'; import { getAdminAWSCredential, @@ -7,80 +7,77 @@ import { getInventory, getInventoryScript, getNotificationTemplate, - getOrCreate, getOrganization, getSmartInventory, getTeam, getUpdatedProject, getUser -} from '../fixtures.js'; +} from '../fixtures'; +const data = {}; +let credentials; +let inventoryScripts; +let notificationTemplates; +let organizations; +let projects; +// let templates; +let users; +let inventories; +let teams; -let data = {}; - -let credentials, - inventoryScripts, - templates, - notificationTemplates, - organizations, - projects, - users, - inventories, - teams; - - -function navigateAndWaitForSpinner(client, url) { +function navigateAndWaitForSpinner (client, url) { client .url(url) .waitForElementVisible('div.spinny') .waitForElementNotVisible('div.spinny'); } - module.exports = { - before: function (client, done) { - all([ - getAuditor().then(obj => data.auditor = obj), - getOrganization().then(obj => data.organization = obj), - getInventory().then(obj => data.inventory = obj), - getInventoryScript().then(obj => data.inventoryScript = obj), - getAdminAWSCredential().then(obj => data.adminAWSCredential = obj), - getAdminMachineCredential().then(obj => data.adminMachineCredential = obj), - getSmartInventory().then(obj => data.smartInventory = obj), - getTeam().then(obj => data.team = obj), - getUser().then(obj => data.user = obj), - getNotificationTemplate().then(obj => data.notificationTemplate = obj), - getUpdatedProject().then(obj => data.project = obj) - ]) - .then(() => { - client.useCss(); + before: (client, done) => { + const promises = [ + getAuditor().then(obj => { data.auditor = obj; }), + getOrganization().then(obj => { data.organization = obj; }), + getInventory().then(obj => { data.inventory = obj; }), + getInventoryScript().then(obj => { data.inventoryScript = obj; }), + getAdminAWSCredential().then(obj => { data.adminAWSCredential = obj; }), + getAdminMachineCredential().then(obj => { data.adminMachineCredential = obj; }), + getSmartInventory().then(obj => { data.smartInventory = obj; }), + getTeam().then(obj => { data.team = obj; }), + getUser().then(obj => { data.user = obj; }), + getNotificationTemplate().then(obj => { data.notificationTemplate = obj; }), + getUpdatedProject().then(obj => { data.project = obj; }) + ]; - credentials = client.page.credentials(); - inventoryScripts = client.page.inventoryScripts(); - templates = client.page.templates(); - notificationTemplates = client.page.notificationTemplates(); - organizations = client.page.organizations(); - projects = client.page.projects(); - users = client.page.users(); - inventories = client.page.inventories(); - teams = client.page.teams(); + all(promises) + .then(() => { + client.useCss(); - client.login(data.auditor.username, data.auditor.password); - client.waitForAngular(); + credentials = client.page.credentials(); + inventoryScripts = client.page.inventoryScripts(); + // templates = client.page.templates(); + notificationTemplates = client.page.notificationTemplates(); + organizations = client.page.organizations(); + projects = client.page.projects(); + users = client.page.users(); + inventories = client.page.inventories(); + teams = client.page.teams(); - done(); - }); + client.login(data.auditor.username, data.auditor.password); + client.waitForAngular(); + + done(); + }); }, - 'verify an auditor\'s credentials inputs are read-only': function (client) { + 'verify an auditor\'s credentials inputs are read-only': client => { navigateAndWaitForSpinner(client, `${credentials.url()}/${data.adminAWSCredential.id}/`); credentials.section.edit .expect.element('@title').text.contain(data.adminAWSCredential.name); - + credentials.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify an auditor\'s inventory scripts inputs are read-only': function (client) { + 'verify an auditor\'s inventory scripts inputs are read-only': client => { navigateAndWaitForSpinner(client, `${inventoryScripts.url()}/${data.inventoryScript.id}/`); inventoryScripts.section.edit @@ -88,13 +85,16 @@ module.exports = { inventoryScripts.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on inventory scripts form': function () { + 'verify save button hidden from auditor on inventory scripts form': () => { inventoryScripts.expect.element('@save').to.not.be.visible; }, - // TODO: re-enable these tests when JT edit has been re-factored to reliably show/remove the loading spinner - // only one time. Without this, we can't tell when all the requisite data is available. + // TODO: re-enable these tests when JT edit has been re-factored to reliably show/remove the + // loading spinner only one time. Without this, we can't tell when all the requisite data is + // available. + // // 'verify an auditor\'s job template inputs are read-only': function (client) { - // navigateAndWaitForSpinner(client, `${templates.url()}/job_template/${data.jobTemplate.id}/`); + // const url = `${templates.url()}/job_template/${data.jobTemplate.id}/`; + // navigateAndWaitForSpinner(client, url); // // templates.section.editJobTemplate // .expect.element('@title').text.contain(data.jobTemplate.name); @@ -104,7 +104,7 @@ module.exports = { // 'verify save button hidden from auditor on job templates form': function () { // templates.expect.element('@save').to.not.be.visible; // }, - 'verify an auditor\'s notification templates inputs are read-only': function (client) { + 'verify an auditor\'s notification templates inputs are read-only': client => { navigateAndWaitForSpinner(client, `${notificationTemplates.url()}/${data.notificationTemplate.id}/`); notificationTemplates.section.edit @@ -112,10 +112,10 @@ module.exports = { notificationTemplates.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on notification templates page': function () { + 'verify save button hidden from auditor on notification templates page': () => { notificationTemplates.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s organizations inputs are read-only': function (client) { + 'verify an auditor\'s organizations inputs are read-only': client => { navigateAndWaitForSpinner(client, `${organizations.url()}/${data.organization.id}/`); organizations.section.edit @@ -123,10 +123,10 @@ module.exports = { organizations.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on organizations form': function () { + 'verify save button hidden from auditor on organizations form': () => { organizations.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s smart inventory inputs are read-only': function (client) { + 'verify an auditor\'s smart inventory inputs are read-only': client => { navigateAndWaitForSpinner(client, `${inventories.url()}/smart/${data.smartInventory.id}/`); inventories.section.editSmartInventory @@ -134,10 +134,10 @@ module.exports = { inventories.section.editSmartInventory.section.smartInvDetails.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on smart inventories form': function () { + 'verify save button hidden from auditor on smart inventories form': () => { inventories.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s project inputs are read-only': function (client) { + 'verify an auditor\'s project inputs are read-only': client => { navigateAndWaitForSpinner(client, `${projects.url()}/${data.project.id}/`); projects.section.edit @@ -145,21 +145,22 @@ module.exports = { projects.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on projects form': function () { + 'verify save button hidden from auditor on projects form': () => { projects.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s standard inventory inputs are read-only': function (client) { + 'verify an auditor\'s standard inventory inputs are read-only': client => { navigateAndWaitForSpinner(client, `${inventories.url()}/inventory/${data.inventory.id}/`); inventories.section.editStandardInventory .expect.element('@title').text.contain(data.inventory.name); - inventories.section.editStandardInventory.section.standardInvDetails.checkAllFieldsDisabled(); + inventories.section.editStandardInventory.section.standardInvDetails + .checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on standard inventory form': function () { + 'verify save button hidden from auditor on standard inventory form': () => { inventories.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s teams inputs are read-only': function (client) { + 'verify an auditor\'s teams inputs are read-only': client => { navigateAndWaitForSpinner(client, `${teams.url()}/${data.team.id}/`); teams.section.edit @@ -167,10 +168,10 @@ module.exports = { teams.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on teams form': function () { + 'verify save button hidden from auditor on teams form': () => { teams.expect.element('@save').to.not.be.visible; }, - 'verify an auditor\'s user inputs are read-only': function (client) { + 'verify an auditor\'s user inputs are read-only': client => { navigateAndWaitForSpinner(client, `${users.url()}/${data.user.id}/`); users.section.edit @@ -178,7 +179,7 @@ module.exports = { users.section.edit.section.details.checkAllFieldsDisabled(); }, - 'verify save button hidden from auditor on users form': function (client) { + 'verify save button hidden from auditor on users form': client => { users.expect.element('@save').to.not.be.visible; client.end(); diff --git a/awx/ui/client/test/e2e/tests/test-credential-types-add-edit.js b/awx/ui/test/e2e/tests/test-credential-types-add-edit.js similarity index 85% rename from awx/ui/client/test/e2e/tests/test-credential-types-add-edit.js rename to awx/ui/test/e2e/tests/test-credential-types-add-edit.js index 120d50eb45..5b343ad0c5 100644 --- a/awx/ui/client/test/e2e/tests/test-credential-types-add-edit.js +++ b/awx/ui/test/e2e/tests/test-credential-types-add-edit.js @@ -1,5 +1,5 @@ module.exports = { - before: function(client, done) { + before: (client, done) => { const credentialTypes = client.page.credentialTypes(); client.login(); @@ -13,9 +13,9 @@ module.exports = { credentialTypes.section.add .waitForElementVisible('@title', done); }, - 'expected fields are present and enabled': function(client) { + 'expected fields are present and enabled': client => { const credentialTypes = client.page.credentialTypes(); - const details = credentialTypes.section.add.section.details; + const { details } = credentialTypes.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-aws.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-aws.js similarity index 76% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-aws.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-aws.js index 6d807cdbc1..d7a3bec3f2 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-aws.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-aws.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,18 +12,17 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -44,9 +42,9 @@ module.exports = { .setValue('@organization', store.organization.name) .setValue('@type', 'Amazon Web Services', done); }, - 'expected fields are visible and enabled': function(client) { + 'expected fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -64,20 +62,23 @@ module.exports = { details.section.aws.expect.element('@secretKey').enabled; details.section.aws.expect.element('@securityToken').enabled; }, - 'required fields display \'*\'': function(client) { + 'required fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; const required = [ details.section.name, details.section.type, details.section.aws.section.accessKey, details.section.aws.section.secretKey - ] - required.map(s => s.expect.element('@label').text.to.contain('*')); + ]; + + required.forEach(s => { + s.expect.element('@label').text.to.contain('*'); + }); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details .clearAndSelectType('Amazon Web Services') @@ -88,10 +89,10 @@ module.exports = { details.section.aws.setValue('@secretKey', 'AAAAAAAAAAAAA'); details.expect.element('@save').enabled; }, - 'create aws credential': function(client) { + 'create aws credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Amazon Web Services') @@ -110,16 +111,16 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); - const search = credentials.section.list.section.search; - const table = credentials.section.list.section.table; + const { search } = credentials.section.list.section; + const { table } = credentials.section.list.section; search .waitForElementVisible('@input') diff --git a/awx/ui/test/e2e/tests/test-credentials-add-edit-custom.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-custom.js new file mode 100644 index 0000000000..7f3caee6a4 --- /dev/null +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-custom.js @@ -0,0 +1,175 @@ +import uuid from 'uuid'; + +const store = { + credentialType: { + name: `credentialType-${uuid().substr(0, 8)}`, + description: 'custom cloud credential', + kind: 'cloud', + inputs: { + fields: [ + { + id: 'project', + label: 'Project', + type: 'string', + help_text: 'Name of your project' + }, + { + id: 'token', + label: 'Token', + secret: true, + type: 'string', + help_text: 'help' + }, + { + id: 'secret_key_data', + label: 'Secret Key', + type: 'string', + secret: true, + multiline: true, + help_text: 'help', + }, + { + id: 'public_key_data', + label: 'Public Key', + type: 'string', + secret: true, + multiline: true, + help_text: 'help', + }, + { + id: 'secret_key_unlock', + label: 'Private Key Passphrase', + type: 'string', + secret: true, + // help_text: 'help' + }, + { + id: 'color', + label: 'Favorite Color', + choices: [ + '', + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'indigo', + 'violet' + ], + help_text: 'help', + }, + ], + required: ['project', 'token'] + }, + injectors: { + env: { + CUSTOM_CREDENTIAL_TOKEN: '{{ token }}' + } + } + } +}; + +const { inputs } = store.credentialType; +const { fields } = inputs; +const help = fields.filter(f => f.help_text); +const required = fields.filter(f => inputs.required.indexOf(f.id) > -1); +const strings = fields.filter(f => f.type === undefined || f.type === 'string'); + +const getObjects = client => { + const credentials = client.page.credentials(); + const { details } = credentials.section.add.section; + const type = details.custom(store.credentialType); + + return { credentials, details, type }; +}; + +module.exports = { + before: (client, done) => { + const credentials = client.page.credentials(); + + client.login(); + client.waitForAngular(); + + client.inject( + [store.credentialType, 'CredentialTypeModel'], + (data, Model) => new Model().http.post(data), + ({ data }) => { store.credentialType.response = data; } + ); + + credentials.section.navigation + .waitForElementVisible('@credentials') + .click('@credentials'); + + credentials + .waitForElementVisible('div.spinny') + .waitForElementNotVisible('div.spinny'); + + credentials.section.list + .waitForElementVisible('@add') + .click('@add'); + + credentials.section.add.section.details + .waitForElementVisible('@save') + .setValue('@name', `cred-${uuid()}`) + .setValue('@type', store.credentialType.name, done); + }, + 'all fields are visible': client => { + const { type } = getObjects(client); + fields.forEach(f => { + type.expect.element(`@${f.id}`).visible; + }); + }, + 'helplinks open popovers showing expected content': client => { + const { type } = getObjects(client); + + help.forEach(f => { + const group = type.section[f.id]; + + group.expect.element('@popover').not.visible; + group.click('@help'); + group.expect.element('@popover').visible; + group.expect.element('@popover').text.to.contain(f.help_text); + group.click('@help'); + }); + + help.forEach(f => { + const group = type.section[f.id]; + group.expect.element('@popover').not.visible; + }); + }, + 'secret field buttons hide and unhide input': client => { + const { type } = getObjects(client); + const secrets = strings.filter(f => f.secret && !f.multiline); + + secrets.forEach(f => { + const group = type.section[f.id]; + const input = `@${f.id}`; + + group.expect.element('@show').visible; + group.expect.element('@hide').not.present; + + type.setValue(input, 'SECRET'); + type.expect.element(input).text.equal(''); + + group.click('@show'); + group.expect.element('@show').not.present; + group.expect.element('@hide').visible; + type.expect.element(input).value.contain('SECRET'); + + group.click('@hide'); + group.expect.element('@show').visible; + group.expect.element('@hide').not.present; + type.expect.element(input).text.equal(''); + }); + }, + 'required fields show * symbol': client => { + const { type } = getObjects(client); + + required.forEach(f => { + const group = type.section[f.id]; + group.expect.element('@label').text.to.contain('*'); + }); + + client.end(); + } +}; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-gce.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-gce.js similarity index 80% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-gce.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-gce.js index 92e39fea0f..f0f59f5ba9 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-gce.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-gce.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,19 +12,18 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -45,9 +43,9 @@ module.exports = { .setValue('@organization', store.organization.name) .setValue('@type', 'Google Compute Engine', done); }, - 'expected fields are visible and enabled': function(client) { + 'expected fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -67,20 +65,23 @@ module.exports = { details.section.organization.expect.element('@lookup').visible; }, - 'required fields display \'*\'': function(client) { + 'required fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; const required = [ details.section.name, details.section.type, details.section.gce.section.email, details.section.gce.section.sshKeyData - ] - required.map(s => s.expect.element('@label').text.to.contain('*')); + ]; + + required.forEach(s => { + s.expect.element('@label').text.to.contain('*'); + }); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details .clearAndSelectType('Google Compute Engine') @@ -98,10 +99,10 @@ module.exports = { details.expect.element('@save').enabled; }, - 'error displayed for invalid ssh key data': function(client) { + 'error displayed for invalid ssh key data': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyData = details.section.gce.section.sshKeyData; + const { details } = credentials.section.add.section; + const { sshKeyData } = details.section.gce.section; details .clearAndSelectType('Google Compute Engine') @@ -124,10 +125,10 @@ module.exports = { details.section.gce.setValue('@sshKeyData', 'AAAA'); sshKeyData.expect.element('@error').not.present; }, - 'create gce credential': function(client) { + 'create gce credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Google Compute Engine') @@ -150,12 +151,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-insights.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-insights.js similarity index 77% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-insights.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-insights.js index 97a3eaed10..a58457b1d7 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-insights.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-insights.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,19 +12,18 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -45,9 +43,9 @@ module.exports = { .setValue('@organization', store.organization.name) .setValue('@type', 'Insights', done); }, - 'expected fields are visible and enabled': function(client) { + 'expected fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -63,20 +61,23 @@ module.exports = { details.section.insights.expect.element('@username').enabled; details.section.insights.expect.element('@password').enabled; }, - 'required fields display \'*\'': function(client) { + 'required fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; const required = [ details.section.name, details.section.type, details.section.insights.section.username, details.section.insights.section.password - ] - required.map(s => s.expect.element('@label').text.to.contain('*')); + ]; + + required.forEach(s => { + s.expect.element('@label').text.to.contain('*'); + }); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details .clearAndSelectType('Insights') @@ -90,10 +91,10 @@ module.exports = { details.expect.element('@save').enabled; }, - 'create insights credential': function(client) { + 'create insights credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Insights') @@ -112,12 +113,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-machine.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-machine.js similarity index 77% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-machine.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-machine.js index 22afd7e4cf..2c1ed44f6d 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-machine.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-machine.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,19 +12,18 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -41,9 +39,9 @@ module.exports = { details.waitForElementVisible('@save', done); }, - 'common fields are visible and enabled': function(client) { + 'common fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -55,16 +53,16 @@ module.exports = { details.expect.element('@organization').enabled; details.expect.element('@type').enabled; }, - 'required common fields display \'*\'': function(client) { + 'required common fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.section.name.expect.element('@label').text.to.contain('*'); details.section.type.expect.element('@label').text.to.contain('*'); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@save').not.enabled; @@ -75,10 +73,10 @@ module.exports = { details.expect.element('@save').enabled; }, - 'machine credential fields are visible after choosing type': function(client) { + 'machine credential fields are visible after choosing type': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const machine = details.section.machine; + const { details } = credentials.section.add.section; + const { machine } = details.section; machine.expect.element('@username').visible; machine.expect.element('@password').visible; @@ -87,10 +85,10 @@ module.exports = { machine.expect.element('@sshKeyData').visible; machine.expect.element('@sshKeyUnlock').visible; }, - 'error displayed for invalid ssh key data': function(client) { + 'error displayed for invalid ssh key data': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyData = details.section.machine.section.sshKeyData; + const { details } = credentials.section.add.section; + const { sshKeyData } = details.section.machine.section; details .clearAndSelectType('Machine') @@ -106,10 +104,10 @@ module.exports = { details.section.machine.clearValue('@sshKeyData'); sshKeyData.expect.element('@error').not.present; }, - 'error displayed for unencrypted ssh key with passphrase': function(client) { + 'error displayed for unencrypted ssh key with passphrase': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyUnlock = details.section.machine.section.sshKeyUnlock; + const { details } = credentials.section.add.section; + const { sshKeyUnlock } = details.section.machine.section; details .clearAndSelectType('Machine') @@ -130,11 +128,11 @@ module.exports = { details.section.machine.clearValue('@sshKeyUnlock'); sshKeyUnlock.expect.element('@error').not.present; - }, - 'create machine credential': function(client) { + }, + 'create machine credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Machine') @@ -161,12 +159,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-network.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-network.js similarity index 76% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-network.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-network.js index 7f71ff8dec..e2cde4e2ce 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-network.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-network.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,19 +12,18 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -41,9 +39,9 @@ module.exports = { details.waitForElementVisible('@save', done); }, - 'common fields are visible and enabled': function(client) { + 'common fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -55,16 +53,16 @@ module.exports = { details.expect.element('@organization').enabled; details.expect.element('@type').enabled; }, - 'required common fields display \'*\'': function(client) { + 'required common fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.section.name.expect.element('@label').text.to.contain('*'); details.section.type.expect.element('@label').text.to.contain('*'); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@save').not.enabled; @@ -79,10 +77,10 @@ module.exports = { details.expect.element('@save').enabled; }, - 'network credential fields are visible after choosing type': function(client) { + 'network credential fields are visible after choosing type': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const network = details.section.network; + const { details } = credentials.section.add.section; + const { network } = details.section; network.expect.element('@username').visible; network.expect.element('@password').visible; @@ -90,10 +88,10 @@ module.exports = { network.expect.element('@sshKeyData').visible; network.expect.element('@sshKeyUnlock').visible; }, - 'error displayed for invalid ssh key data': function(client) { + 'error displayed for invalid ssh key data': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyData = details.section.network.section.sshKeyData; + const { details } = credentials.section.add.section; + const { sshKeyData } = details.section.network.section; details .clearAndSelectType('Network') @@ -111,10 +109,10 @@ module.exports = { details.section.network.clearValue('@sshKeyData'); sshKeyData.expect.element('@error').not.present; }, - 'error displayed for unencrypted ssh key with passphrase': function(client) { + 'error displayed for unencrypted ssh key with passphrase': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyUnlock = details.section.network.section.sshKeyUnlock; + const { details } = credentials.section.add.section; + const { sshKeyUnlock } = details.section.network.section; details .clearAndSelectType('Network') @@ -136,11 +134,11 @@ module.exports = { details.section.network.clearValue('@sshKeyUnlock'); sshKeyUnlock.expect.element('@error').not.present; - }, - 'error displayed for authorize password without authorize enabled': function(client) { + }, + 'error displayed for authorize password without authorize enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const authorizePassword = details.section.network.section.authorizePassword; + const { details } = credentials.section.add.section; + const { authorizePassword } = details.section.network.section; details .clearAndSelectType('Network') @@ -152,17 +150,17 @@ module.exports = { details.click('@save'); - let expected = 'cannot be set unless "Authorize" is set'; + const expected = 'cannot be set unless "Authorize" is set'; authorizePassword.expect.element('@error').visible; authorizePassword.expect.element('@error').text.to.equal(expected); details.section.network.clearValue('@authorizePassword'); authorizePassword.expect.element('@error').not.present; - }, - 'create network credential': function(client) { + }, + 'create network credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Network') @@ -186,12 +184,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-scm.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-scm.js similarity index 76% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-scm.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-scm.js index cdcc438f46..9231907afc 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-scm.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-scm.js @@ -1,9 +1,8 @@ import uuid from 'uuid'; +const testID = uuid().substr(0, 8); -let testID = uuid().substr(0,8); - -let store = { +const store = { organization: { name: `org-${testID}` }, @@ -13,19 +12,18 @@ let store = { }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -41,9 +39,9 @@ module.exports = { details.waitForElementVisible('@save', done); }, - 'common fields are visible and enabled': function(client) { + 'common fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -55,16 +53,16 @@ module.exports = { details.expect.element('@organization').enabled; details.expect.element('@type').enabled; }, - 'required common fields display \'*\'': function(client) { + 'required common fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.section.name.expect.element('@label').text.to.contain('*'); details.section.type.expect.element('@label').text.to.contain('*'); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@save').not.enabled; @@ -75,19 +73,19 @@ module.exports = { details.expect.element('@save').enabled; }, - 'scm credential fields are visible after choosing type': function(client) { + 'scm credential fields are visible after choosing type': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.section.scm.expect.element('@username').visible; details.section.scm.expect.element('@password').visible; details.section.scm.expect.element('@sshKeyData').visible; details.section.scm.expect.element('@sshKeyUnlock').visible; }, - 'error displayed for invalid ssh key data': function(client) { + 'error displayed for invalid ssh key data': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyData = details.section.scm.section.sshKeyData; + const { details } = credentials.section.add.section; + const { sshKeyData } = details.section.scm.section; details .clearAndSelectType('Source Control') @@ -103,10 +101,10 @@ module.exports = { details.section.scm.clearValue('@sshKeyData'); sshKeyData.expect.element('@error').not.present; }, - 'error displayed for unencrypted ssh key with passphrase': function(client) { + 'error displayed for unencrypted ssh key with passphrase': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const sshKeyUnlock = details.section.scm.section.sshKeyUnlock; + const { details } = credentials.section.add.section; + const { sshKeyUnlock } = details.section.scm.section; details .clearAndSelectType('Source Control') @@ -127,11 +125,11 @@ module.exports = { details.section.scm.clearValue('@sshKeyUnlock'); sshKeyUnlock.expect.element('@error').not.present; - }, - 'create SCM credential': function(client) { + }, + 'create SCM credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Source Control') @@ -155,12 +153,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-vault.js b/awx/ui/test/e2e/tests/test-credentials-add-edit-vault.js similarity index 77% rename from awx/ui/client/test/e2e/tests/test-credentials-add-edit-vault.js rename to awx/ui/test/e2e/tests/test-credentials-add-edit-vault.js index 153f4984ce..9987666e0f 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-add-edit-vault.js +++ b/awx/ui/test/e2e/tests/test-credentials-add-edit-vault.js @@ -1,31 +1,28 @@ import uuid from 'uuid'; - -let testID = uuid().substr(0,8); - -let store = { +const testID = uuid().substr(0, 8); +const store = { organization: { name: `org-${testID}` }, credential: { name: `cred-${testID}` - }, + } }; module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); - client.inject([store, 'OrganizationModel'], (store, model) => { - return new model().http.post(store.organization); - }, - ({ data }) => { - store.organization = data; - }); + client.inject( + [store, 'OrganizationModel'], + (_store_, Model) => new Model().http.post(_store_.organization), + ({ data }) => { store.organization = data; } + ); credentials.section.navigation .waitForElementVisible('@credentials') @@ -45,9 +42,9 @@ module.exports = { .setValue('@organization', store.organization.name) .setValue('@type', 'Vault', done); }, - 'expected fields are visible and enabled': function(client) { + 'expected fields are visible and enabled': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details.expect.element('@name').visible; details.expect.element('@description').visible; @@ -61,19 +58,20 @@ module.exports = { details.expect.element('@type').enabled; details.section.vault.expect.element('@vaultPassword').enabled; }, - 'required fields display \'*\'': function(client) { + 'required fields display \'*\'': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; const required = [ details.section.name, details.section.type, details.section.vault.section.vaultPassword, - ] + ]; + required.map(s => s.expect.element('@label').text.to.contain('*')); }, - 'save button becomes enabled after providing required fields': function(client) { + 'save button becomes enabled after providing required fields': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details .clearAndSelectType('Vault') @@ -83,9 +81,9 @@ module.exports = { details.section.vault.setValue('@vaultPassword', 'ch@ng3m3'); details.expect.element('@save').enabled; }, - 'vault password field is disabled when prompt on launch is selected': function(client) { + 'vault password field is disabled when prompt on launch is selected': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; details .clearAndSelectType('Vault') @@ -95,10 +93,10 @@ module.exports = { details.section.vault.section.vaultPassword.click('@prompt'); details.section.vault.expect.element('@vaultPassword').not.enabled; }, - 'create vault credential': function(client) { + 'create vault credential': client => { const credentials = client.page.credentials(); - const add = credentials.section.add; - const edit = credentials.section.edit; + const { add } = credentials.section; + const { edit } = credentials.section; add.section.details .clearAndSelectType('Vault') @@ -115,12 +113,12 @@ module.exports = { edit.expect.element('@title').text.equal(store.credential.name); }, - 'edit details panel remains open after saving': function(client) { + 'edit details panel remains open after saving': client => { const credentials = client.page.credentials(); credentials.section.edit.expect.section('@details').visible; }, - 'credential is searchable after saving': function(client) { + 'credential is searchable after saving': client => { const credentials = client.page.credentials(); const row = '#credentials_table tbody tr'; diff --git a/awx/ui/client/test/e2e/tests/test-credentials-lookup-credential-type.js b/awx/ui/test/e2e/tests/test-credentials-lookup-credential-type.js similarity index 78% rename from awx/ui/client/test/e2e/tests/test-credentials-lookup-credential-type.js rename to awx/ui/test/e2e/tests/test-credentials-lookup-credential-type.js index 7bed8eed7b..72b1c891a1 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-lookup-credential-type.js +++ b/awx/ui/test/e2e/tests/test-credentials-lookup-credential-type.js @@ -1,7 +1,7 @@ module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); @@ -19,11 +19,11 @@ module.exports = { .click('@add'); details - .waitForElementVisible('@save', done) + .waitForElementVisible('@save', done); }, - 'open the lookup modal': function(client) { + 'open the lookup modal': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; const modal = 'div[class="modal-body"]'; const title = 'div[class^="Form-title"]'; @@ -38,7 +38,7 @@ module.exports = { client.expect.element(modal).present; - let expected = 'SELECT CREDENTIAL TYPE'; + const expected = 'SELECT CREDENTIAL TYPE'; client.expect.element(title).visible; client.expect.element(title).text.equal(expected); diff --git a/awx/ui/client/test/e2e/tests/test-credentials-lookup-organization.js b/awx/ui/test/e2e/tests/test-credentials-lookup-organization.js similarity index 85% rename from awx/ui/client/test/e2e/tests/test-credentials-lookup-organization.js rename to awx/ui/test/e2e/tests/test-credentials-lookup-organization.js index 9b8f2ecc6e..5b62fac9ac 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-lookup-organization.js +++ b/awx/ui/test/e2e/tests/test-credentials-lookup-organization.js @@ -1,7 +1,7 @@ module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; + const { details } = credentials.section.add.section; client.login(); client.waitForAngular(); @@ -20,12 +20,11 @@ module.exports = { details .waitForElementVisible('@save', done); - }, - 'open the lookup modal': function(client) { + 'open the lookup modal': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const lookupModal = credentials.section.lookupModal; + const { details } = credentials.section.add.section; + const { lookupModal } = credentials.section; details.expect.element('@organization').visible; details.expect.element('@organization').enabled; @@ -37,15 +36,15 @@ module.exports = { credentials.expect.section('@lookupModal').present; - let expected = 'SELECT ORGANIZATION'; + const expected = 'SELECT ORGANIZATION'; lookupModal.expect.element('@title').visible; lookupModal.expect.element('@title').text.equal(expected); }, - 'select button is disabled until item is selected': function(client) { + 'select button is disabled until item is selected': client => { const credentials = client.page.credentials(); - const details = credentials.section.add.section.details; - const lookupModal = credentials.section.lookupModal; - const table = lookupModal.section.table; + const { details } = credentials.section.add.section; + const { lookupModal } = credentials.section; + const { table } = lookupModal.section; details.section.organization.expect.element('@lookup').visible; details.section.organization.expect.element('@lookup').enabled; @@ -70,12 +69,12 @@ module.exports = { lookupModal.expect.element('@select').visible; lookupModal.expect.element('@select').enabled; }, - 'sort and unsort the table by name with an item selected': function(client) { + 'sort and unsort the table by name with an item selected': client => { const credentials = client.page.credentials(); - const lookupModal = credentials.section.lookupModal; - const table = lookupModal.section.table; + const { lookupModal } = credentials.section; + const { table } = lookupModal.section; - let column = table.section.header.findColumnByText('Name'); + const column = table.section.header.findColumnByText('Name'); column.expect.element('@self').visible; column.expect.element('@sortable').visible; @@ -100,11 +99,11 @@ module.exports = { table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected; table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected; }, - 'use the pagination controls with an item selected': function(client) { + 'use the pagination controls with an item selected': client => { const credentials = client.page.credentials(); - const lookupModal = credentials.section.lookupModal; - const table = lookupModal.section.table; - const pagination = lookupModal.section.pagination; + const { lookupModal } = credentials.section; + const { table } = lookupModal.section; + const { pagination } = lookupModal.section; pagination.click('@next'); credentials.waitForElementVisible('div.spinny'); diff --git a/awx/ui/client/test/e2e/tests/test-credentials-navigation-click-through.js b/awx/ui/test/e2e/tests/test-credentials-navigation-click-through.js similarity index 94% rename from awx/ui/client/test/e2e/tests/test-credentials-navigation-click-through.js rename to awx/ui/test/e2e/tests/test-credentials-navigation-click-through.js index f9f618756f..19eae2dd9d 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-navigation-click-through.js +++ b/awx/ui/test/e2e/tests/test-credentials-navigation-click-through.js @@ -1,5 +1,5 @@ module.exports = { - beforeEach: function(client, done) { + beforeEach: (client, done) => { const credentials = client.useCss().page.credentials(); client.login(); @@ -10,7 +10,7 @@ module.exports = { .waitForElementVisible('div.spinny') .waitForElementNotVisible('div.spinny', done); }, - 'activity link is visible and takes user to activity stream': function(client) { + 'activity link is visible and takes user to activity stream': client => { const credentials = client.page.credentials(); const activityStream = client.page.activityStream(); diff --git a/awx/ui/client/test/e2e/tests/test-credentials-search-sort.js b/awx/ui/test/e2e/tests/test-credentials-search-sort.js similarity index 65% rename from awx/ui/client/test/e2e/tests/test-credentials-search-sort.js rename to awx/ui/test/e2e/tests/test-credentials-search-sort.js index 12a8d79ef4..525901b4ae 100644 --- a/awx/ui/client/test/e2e/tests/test-credentials-search-sort.js +++ b/awx/ui/test/e2e/tests/test-credentials-search-sort.js @@ -1,10 +1,8 @@ const columns = ['Name', 'Kind', 'Owners', 'Actions']; const sortable = ['Name']; -const defaultSorted = ['Name']; - module.exports = { - before: function(client, done) { + before: (client, done) => { const credentials = client.page.credentials(); client.login(); @@ -18,30 +16,30 @@ module.exports = { credentials.waitForElementVisible('#credentials_table', done); }, - 'expected table columns are visible': function(client) { + 'expected table columns are visible': client => { const credentials = client.page.credentials(); - const table = credentials.section.list.section.table; + const { table } = credentials.section.list.section; - columns.map(label => { + columns.forEach(label => { table.section.header.findColumnByText(label) .expect.element('@self').visible; }); }, - 'only fields expected to be sortable show sort icon': function(client) { + 'only fields expected to be sortable show sort icon': client => { const credentials = client.page.credentials(); - const table = credentials.section.list.section.table; + const { table } = credentials.section.list.section; - sortable.map(label => { + sortable.forEach(label => { table.section.header.findColumnByText(label) .expect.element('@sortable').visible; }); }, - 'sort all columns expected to be sortable': function(client) { + 'sort all columns expected to be sortable': client => { const credentials = client.page.credentials(); - const table = credentials.section.list.section.table; + const { table } = credentials.section.list.section; - sortable.map(label => { - let column = table.section.header.findColumnByText(label); + sortable.forEach(label => { + const column = table.section.header.findColumnByText(label); column.click('@self'); diff --git a/awx/ui/tests/spec/column-sort/column-sort.directive-test.js b/awx/ui/test/spec/column-sort/column-sort.directive-test.js similarity index 98% rename from awx/ui/tests/spec/column-sort/column-sort.directive-test.js rename to awx/ui/test/spec/column-sort/column-sort.directive-test.js index eff9ddb378..64b0eb4971 100644 --- a/awx/ui/tests/spec/column-sort/column-sort.directive-test.js +++ b/awx/ui/test/spec/column-sort/column-sort.directive-test.js @@ -1,6 +1,6 @@ 'use strict'; -describe('Directive: column-sort', () =>{ +xdescribe('Directive: column-sort', () =>{ let $scope, template, $compile, QuerySet, GetBasePath; diff --git a/awx/ui/tests/spec/features/features.directive-test.js b/awx/ui/test/spec/features/features.directive-test.js similarity index 100% rename from awx/ui/tests/spec/features/features.directive-test.js rename to awx/ui/test/spec/features/features.directive-test.js diff --git a/awx/ui/tests/spec/inventories/insights/data/high.insights-data.js b/awx/ui/test/spec/inventories/insights/data/high.insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/high.insights-data.js rename to awx/ui/test/spec/inventories/insights/data/high.insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/data/insights-data.js b/awx/ui/test/spec/inventories/insights/data/insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/insights-data.js rename to awx/ui/test/spec/inventories/insights/data/insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/data/low.insights-data.js b/awx/ui/test/spec/inventories/insights/data/low.insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/low.insights-data.js rename to awx/ui/test/spec/inventories/insights/data/low.insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/data/medium.insights-data.js b/awx/ui/test/spec/inventories/insights/data/medium.insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/medium.insights-data.js rename to awx/ui/test/spec/inventories/insights/data/medium.insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/data/not_solvable.insights-data.js b/awx/ui/test/spec/inventories/insights/data/not_solvable.insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/not_solvable.insights-data.js rename to awx/ui/test/spec/inventories/insights/data/not_solvable.insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/data/solvable.insights-data.js b/awx/ui/test/spec/inventories/insights/data/solvable.insights-data.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/data/solvable.insights-data.js rename to awx/ui/test/spec/inventories/insights/data/solvable.insights-data.js diff --git a/awx/ui/tests/spec/inventories/insights/insights.service-test.js b/awx/ui/test/spec/inventories/insights/insights.service-test.js similarity index 100% rename from awx/ui/tests/spec/inventories/insights/insights.service-test.js rename to awx/ui/test/spec/inventories/insights/insights.service-test.js diff --git a/awx/ui/tests/spec/inventories/manage/inventory-manage.service-test.js b/awx/ui/test/spec/inventories/manage/inventory-manage.service-test.js similarity index 100% rename from awx/ui/tests/spec/inventories/manage/inventory-manage.service-test.js rename to awx/ui/test/spec/inventories/manage/inventory-manage.service-test.js diff --git a/awx/ui/tests/spec/job-results/job-results.controller-test.js b/awx/ui/test/spec/job-results/job-results.controller-test.js similarity index 100% rename from awx/ui/tests/spec/job-results/job-results.controller-test.js rename to awx/ui/test/spec/job-results/job-results.controller-test.js diff --git a/awx/ui/tests/spec/job-results/job-results.service-test.js b/awx/ui/test/spec/job-results/job-results.service-test.js similarity index 100% rename from awx/ui/tests/spec/job-results/job-results.service-test.js rename to awx/ui/test/spec/job-results/job-results.service-test.js diff --git a/awx/ui/tests/spec/job-results/parse-stdout.service-test.js b/awx/ui/test/spec/job-results/parse-stdout.service-test.js similarity index 100% rename from awx/ui/tests/spec/job-results/parse-stdout.service-test.js rename to awx/ui/test/spec/job-results/parse-stdout.service-test.js diff --git a/awx/ui/test/spec/karma.spec.js b/awx/ui/test/spec/karma.spec.js new file mode 100644 index 0000000000..4d8bfc8101 --- /dev/null +++ b/awx/ui/test/spec/karma.spec.js @@ -0,0 +1,45 @@ +const path = require('path'); + +const SRC_PATH = path.resolve(__dirname, '../../client/src'); +const NODE_MODULES = path.resolve(__dirname, '../../node_modules'); + +const webpackConfig = require('./webpack.spec'); + +module.exports = function(config) { + config.set({ + autoWatch: true, + colors: true, + browsers: ['Chrome', 'Firefox'], + coverageReporter: { + reporters: [ + { type: 'html', subdir: 'html' }, + ] + }, + frameworks: [ + 'jasmine', + ], + reporters: ['progress', 'coverage', 'junit'], + files:[ + path.join(SRC_PATH, '**/*.html'), + path.join(SRC_PATH, 'vendor.js'), + path.join(NODE_MODULES, 'angular-mocks/angular-mocks.js'), + path.join(SRC_PATH, 'app.js'), + '**/*-test.js', + ], + preprocessors: { + [path.join(SRC_PATH, '**/*.html')]: 'html2js', + [path.join(SRC_PATH, 'vendor.js')]: 'webpack', + [path.join(SRC_PATH, 'app.js')]: 'webpack', + '**/*-test.js': 'webpack' + }, + webpack: webpackConfig, + webpackMiddleware: { + noInfo: true + }, + junitReporter: { + outputDir: 'coverage', + outputFile: 'ui-unit-test-results.xml', + useBrowserName: false + } + }); +}; diff --git a/awx/ui/tests/spec/license/license.controller-test.js b/awx/ui/test/spec/license/license.controller-test.js similarity index 100% rename from awx/ui/tests/spec/license/license.controller-test.js rename to awx/ui/test/spec/license/license.controller-test.js diff --git a/awx/ui/tests/spec/lookup/lookup-modal.directive-test.js b/awx/ui/test/spec/lookup/lookup-modal.directive-test.js similarity index 100% rename from awx/ui/tests/spec/lookup/lookup-modal.directive-test.js rename to awx/ui/test/spec/lookup/lookup-modal.directive-test.js diff --git a/awx/ui/tests/spec/multi-credential/multi-credential.service-test.js b/awx/ui/test/spec/multi-credential/multi-credential.service-test.js similarity index 100% rename from awx/ui/tests/spec/multi-credential/multi-credential.service-test.js rename to awx/ui/test/spec/multi-credential/multi-credential.service-test.js diff --git a/awx/ui/tests/spec/paginate/paginate.directive-test.js b/awx/ui/test/spec/paginate/paginate.directive-test.js similarity index 100% rename from awx/ui/tests/spec/paginate/paginate.directive-test.js rename to awx/ui/test/spec/paginate/paginate.directive-test.js diff --git a/awx/ui/tests/spec/shared/filters/append.filter-test.js b/awx/ui/test/spec/shared/filters/append.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/append.filter-test.js rename to awx/ui/test/spec/shared/filters/append.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/capitalize.filter-test.js b/awx/ui/test/spec/shared/filters/capitalize.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/capitalize.filter-test.js rename to awx/ui/test/spec/shared/filters/capitalize.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/format-epoch.filter-test.js b/awx/ui/test/spec/shared/filters/format-epoch.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/format-epoch.filter-test.js rename to awx/ui/test/spec/shared/filters/format-epoch.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/is-empty.filter-test.js b/awx/ui/test/spec/shared/filters/is-empty.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/is-empty.filter-test.js rename to awx/ui/test/spec/shared/filters/is-empty.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/long-date.filter-test.js b/awx/ui/test/spec/shared/filters/long-date.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/long-date.filter-test.js rename to awx/ui/test/spec/shared/filters/long-date.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/prepend.filter-test.js b/awx/ui/test/spec/shared/filters/prepend.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/prepend.filter-test.js rename to awx/ui/test/spec/shared/filters/prepend.filter-test.js diff --git a/awx/ui/tests/spec/shared/filters/xss-sanitizer.filter-test.js b/awx/ui/test/spec/shared/filters/xss-sanitizer.filter-test.js similarity index 100% rename from awx/ui/tests/spec/shared/filters/xss-sanitizer.filter-test.js rename to awx/ui/test/spec/shared/filters/xss-sanitizer.filter-test.js diff --git a/awx/ui/tests/spec/smart-search/queryset.service-test.js b/awx/ui/test/spec/smart-search/queryset.service-test.js similarity index 100% rename from awx/ui/tests/spec/smart-search/queryset.service-test.js rename to awx/ui/test/spec/smart-search/queryset.service-test.js diff --git a/awx/ui/tests/spec/smart-search/smart-search.directive-test.js b/awx/ui/test/spec/smart-search/smart-search.directive-test.js similarity index 100% rename from awx/ui/tests/spec/smart-search/smart-search.directive-test.js rename to awx/ui/test/spec/smart-search/smart-search.directive-test.js diff --git a/awx/ui/tests/spec/smart-search/smart-search.service-test.js b/awx/ui/test/spec/smart-search/smart-search.service-test.js similarity index 100% rename from awx/ui/tests/spec/smart-search/smart-search.service-test.js rename to awx/ui/test/spec/smart-search/smart-search.service-test.js diff --git a/awx/ui/tests/spec/socket/socket.service-test.js b/awx/ui/test/spec/socket/socket.service-test.js similarity index 100% rename from awx/ui/tests/spec/socket/socket.service-test.js rename to awx/ui/test/spec/socket/socket.service-test.js diff --git a/awx/ui/tests/spec/templates/templates-list.controller-test.js b/awx/ui/test/spec/templates/templates-list.controller-test.js similarity index 100% rename from awx/ui/tests/spec/templates/templates-list.controller-test.js rename to awx/ui/test/spec/templates/templates-list.controller-test.js diff --git a/awx/ui/test/spec/webpack.spec.js b/awx/ui/test/spec/webpack.spec.js new file mode 100644 index 0000000000..6ed0f049ad --- /dev/null +++ b/awx/ui/test/spec/webpack.spec.js @@ -0,0 +1,18 @@ +const path = require('path'); + +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const base = require(path.resolve(__dirname, '../..', 'build/webpack.base')); + +const STATIC_URL = '/static/'; + +const test = { + devtool: 'inline-source-map', + plugins: [ + new webpack.DefinePlugin({ + $basePath: STATIC_URL + }) + ] +}; + +module.exports = merge(base, test); diff --git a/awx/ui/tests/spec/workflow--results/data/workflow_job.js b/awx/ui/test/spec/workflow--results/data/workflow_job.js similarity index 100% rename from awx/ui/tests/spec/workflow--results/data/workflow_job.js rename to awx/ui/test/spec/workflow--results/data/workflow_job.js diff --git a/awx/ui/tests/spec/workflow--results/data/workflow_job_options.js b/awx/ui/test/spec/workflow--results/data/workflow_job_options.js similarity index 100% rename from awx/ui/tests/spec/workflow--results/data/workflow_job_options.js rename to awx/ui/test/spec/workflow--results/data/workflow_job_options.js diff --git a/awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js b/awx/ui/test/spec/workflow--results/workflow-results.controller-test.js similarity index 100% rename from awx/ui/tests/spec/workflow--results/workflow-results.controller-test.js rename to awx/ui/test/spec/workflow--results/workflow-results.controller-test.js diff --git a/awx/ui/tests/spec/workflow--results/workflow-results.service-test.js b/awx/ui/test/spec/workflow--results/workflow-results.service-test.js similarity index 100% rename from awx/ui/tests/spec/workflow--results/workflow-results.service-test.js rename to awx/ui/test/spec/workflow--results/workflow-results.service-test.js diff --git a/awx/ui/tests/spec/workflows/workflow-add.controller-test.js b/awx/ui/test/spec/workflows/workflow-add.controller-test.js similarity index 100% rename from awx/ui/tests/spec/workflows/workflow-add.controller-test.js rename to awx/ui/test/spec/workflows/workflow-add.controller-test.js diff --git a/awx/ui/tests/spec/workflows/workflow-maker.controller-test.js b/awx/ui/test/spec/workflows/workflow-maker.controller-test.js similarity index 100% rename from awx/ui/tests/spec/workflows/workflow-maker.controller-test.js rename to awx/ui/test/spec/workflows/workflow-maker.controller-test.js diff --git a/awx/ui/test/unit/.eslintrc.js b/awx/ui/test/unit/.eslintrc.js new file mode 100644 index 0000000000..c8aebfd813 --- /dev/null +++ b/awx/ui/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + jasmine: true + } +}; + diff --git a/awx/ui/client/test/unit/index.js b/awx/ui/test/unit/components/index.js similarity index 54% rename from awx/ui/client/test/unit/index.js rename to awx/ui/test/unit/components/index.js index d26d4c6870..398445bc6f 100644 --- a/awx/ui/client/test/unit/index.js +++ b/awx/ui/test/unit/components/index.js @@ -2,6 +2,7 @@ import 'angular-mocks'; // Import tests -import './layout.spec'; -import './side-nav.spec'; -import './side-nav-item.spec'; \ No newline at end of file +import './layout.unit'; +import './side-nav.unit'; +import './side-nav-item.unit'; + diff --git a/awx/ui/test/unit/components/layout.unit.js b/awx/ui/test/unit/components/layout.unit.js new file mode 100644 index 0000000000..9e586d5a40 --- /dev/null +++ b/awx/ui/test/unit/components/layout.unit.js @@ -0,0 +1,160 @@ +describe('Components | Layout', () => { + let $compile; + let $rootScope; + let element; + let scope; + + beforeEach(() => { + angular.mock.module('gettext'); + angular.mock.module('I18N'); + angular.mock.module('ui.router'); + angular.mock.module('at.lib.services'); + angular.mock.module('at.lib.components'); + }); + + beforeEach(angular.mock.inject((_$compile_, _$rootScope_) => { + $compile = _$compile_; + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + + element = angular.element(''); + element = $compile(element)(scope); + scope.$digest(); + })); + + describe('AtLayoutController', () => { + let controller; + + beforeEach(() => { + controller = element.controller('atLayout'); + }); + + it('$scope.$on($stateChangeSuccess) should assign toState name to currentState', () => { + const next = { name: 'dashboard' }; + $rootScope.$broadcast('$stateChangeSuccess', next); + expect(controller.currentState).toBe('dashboard'); + }); + + describe('$root.current_user watcher should assign value to ', () => { + beforeEach(() => { + const val = { + username: 'admin', + id: 1 + }; + $rootScope.current_user = val; + scope.$digest(); + }); + + it('isLoggedIn', () => { + expect(controller.isLoggedIn).toBe('admin'); + + $rootScope.current_user = { id: 1 }; + scope.$digest(); + expect(controller.isLoggedIn).not.toBeDefined(); + }); + + it('isSuperUser', () => { + $rootScope.current_user = 'one'; + $rootScope.user_is_superuser = true; + $rootScope.user_is_system_auditor = false; + scope.$digest(); + expect(controller.isSuperUser).toBe(true); + + $rootScope.current_user = 'two'; + $rootScope.user_is_superuser = false; + $rootScope.user_is_system_auditor = true; + scope.$digest(); + expect(controller.isSuperUser).toBe(true); + + $rootScope.current_user = 'three'; + $rootScope.user_is_superuser = true; + $rootScope.user_is_system_auditor = true; + scope.$digest(); + expect(controller.isSuperUser).toBe(true); + + $rootScope.current_user = 'four'; + $rootScope.user_is_superuser = false; + $rootScope.user_is_system_auditor = false; + scope.$digest(); + expect(controller.isSuperUser).toBe(false); + }); + + it('currentUsername', () => { + expect(controller.currentUsername).toBeTruthy(); + expect(controller.currentUsername).toBe('admin'); + }); + + it('currentUserId', () => { + expect(controller.currentUserId).toBeTruthy(); + expect(controller.currentUserId).toBe(1); + }); + }); + + describe('$root.socketStatus watcher should assign newStatus to', () => { + const statuses = ['connecting', 'error', 'ok']; + + it('socketState', () => { + _.forEach(statuses, (status) => { + $rootScope.socketStatus = status; + scope.$digest(); + expect(controller.socketState).toBeTruthy(); + expect(controller.socketState).toBe(status); + }); + }); + + it('socketIconClass', () => { + _.forEach(statuses, (status) => { + $rootScope.socketStatus = status; + scope.$digest(); + expect(controller.socketIconClass).toBe(`icon-socket-${status}`); + }); + }); + }); + + describe('$root.licenseMissing watcher should assign true or false to', () => { + it('licenseIsMissing', () => { + $rootScope.licenseMissing = true; + scope.$digest(); + expect(controller.licenseIsMissing).toBe(true); + + $rootScope.licenseMissing = false; + scope.$digest(); + expect(controller.licenseIsMissing).toBe(false); + }); + }); + + describe('getString()', () => { + it('calls ComponentsStrings get() method', angular.mock.inject((_ComponentsStrings_) => { + spyOn(_ComponentsStrings_, 'get'); + controller.getString('VIEW_DOCS'); + expect(_ComponentsStrings_.get).toHaveBeenCalled(); + })); + + it('ComponentsStrings get() method should throw an error if string is not a property name of the layout class', () => { + expect(controller.getString.bind(null, 'SUBMISSION_ERROR_TITLE')).toThrow(); + }); + + it('should return layout string', () => { + const layoutStrings = { + CURRENT_USER_LABEL: 'Logged in as', + VIEW_DOCS: 'View Documentation', + LOGOUT: 'Logout', + }; + + _.forEach(layoutStrings, (value, key) => { + expect(controller.getString(key)).toBe(value); + }); + }); + + it('should return default string', () => { + const defaultStrings = { + BRAND_NAME: 'AWX' + }; + + _.forEach(defaultStrings, (value, key) => { + expect(controller.getString(key)).toBe(value); + }); + }); + }); + }); +}); diff --git a/awx/ui/test/unit/components/side-nav-item.unit.js b/awx/ui/test/unit/components/side-nav-item.unit.js new file mode 100644 index 0000000000..ac5631404d --- /dev/null +++ b/awx/ui/test/unit/components/side-nav-item.unit.js @@ -0,0 +1,60 @@ +describe('Components | Side Nav Item', () => { + let $compile; + let $rootScope; + let element; + let scope; + + beforeEach(() => { + angular.mock.module('gettext'); + angular.mock.module('I18N'); + angular.mock.module('ui.router'); + angular.mock.module('at.lib.services'); + angular.mock.module('at.lib.components'); + }); + + beforeEach(angular.mock.inject((_$compile_, _$rootScope_) => { + $compile = _$compile_; + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + + element = angular.element(''); + element = $compile(element)(scope); + scope.name = 'dashboard'; + scope.$digest(); + })); + + describe('Side Nav Item Controller', () => { + let SideNavItem; + let SideNavItemCtrl; + + beforeEach(() => { + SideNavItem = angular.element(element[0].querySelector('at-side-nav-item')); + SideNavItemCtrl = SideNavItem.controller('atSideNavItem'); + }); + + it('layoutVm.currentState watcher should assign isRoute', () => { + let current = { name: 'dashboard' }; + $rootScope.$broadcast('$stateChangeSuccess', current); + scope.$digest(); + expect(SideNavItemCtrl.isRoute).toBe(true); + + current = { name: 'inventories' }; + $rootScope.$broadcast('$stateChangeSuccess', current); + scope.$digest(); + expect(SideNavItemCtrl.isRoute).toBe(false); + }); + + it('go() should call $state.go()', angular.mock.inject((_$state_) => { + spyOn(_$state_, 'go'); + SideNavItemCtrl.go(); + expect(_$state_.go).toHaveBeenCalled(); + expect(_$state_.go).toHaveBeenCalledWith('dashboard', jasmine.any(Object), jasmine.any(Object)); + })); + + it('should load name, icon, and route from scope', () => { + expect(SideNavItem.isolateScope().name).toBeDefined(); + expect(SideNavItem.isolateScope().iconClass).toBeDefined(); + expect(SideNavItem.isolateScope().route).toBeDefined(); + }); + }); +}); diff --git a/awx/ui/test/unit/components/side-nav.unit.js b/awx/ui/test/unit/components/side-nav.unit.js new file mode 100644 index 0000000000..03d0130a96 --- /dev/null +++ b/awx/ui/test/unit/components/side-nav.unit.js @@ -0,0 +1,78 @@ +describe('Components | Side Nav', () => { + let $compile; + let $rootScope; + let element; + let scope; + const windowMock = { + innerWidth: 500 + }; + + beforeEach(() => { + angular.mock.module('gettext'); + angular.mock.module('I18N'); + angular.mock.module('ui.router'); + angular.mock.module('at.lib.services'); + angular.mock.module('at.lib.components', ($provide) => { + $provide.value('$window', windowMock); + }); + }); + + beforeEach(angular.mock.inject((_$compile_, _$rootScope_) => { + $compile = _$compile_; + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + + element = angular.element(''); + element = $compile(element)(scope); + scope.$digest(); + })); + + describe('Side Nav Controller', () => { + let sideNav; + let sideNavCtrl; + + beforeEach(() => { + sideNav = angular.element(element[0].querySelector('.at-Layout-side')); + sideNavCtrl = sideNav.controller('atSideNav'); + }); + + it('isExpanded defaults to false', () => { + expect(sideNavCtrl.isExpanded).toBe(false); + }); + + it('toggleExpansion()', () => { + expect(sideNavCtrl.isExpanded).toBe(false); + + sideNavCtrl.toggleExpansion(); + expect(sideNavCtrl.isExpanded).toBe(true); + + sideNavCtrl.toggleExpansion(); + expect(sideNavCtrl.isExpanded).toBe(false); + + sideNavCtrl.toggleExpansion(); + expect(sideNavCtrl.isExpanded).toBe(true); + + sideNavCtrl.toggleExpansion(); + expect(sideNavCtrl.isExpanded).toBe(false); + }); + + it('isExpanded should be false after state change event', () => { + sideNavCtrl.isExpanded = true; + + const current = { + name: 'dashboard' + }; + $rootScope.$broadcast('$stateChangeSuccess', current); + scope.$digest(); + expect(sideNavCtrl.isExpanded).toBe(false); + }); + + it('clickOutsideSideNav watcher should assign isExpanded to false', () => { + sideNavCtrl.isExpanded = true; + + $rootScope.$broadcast('clickOutsideSideNav'); + scope.$digest(); + expect(sideNavCtrl.isExpanded).toBe(false); + }); + }); +}); diff --git a/awx/ui/test/unit/index.js b/awx/ui/test/unit/index.js new file mode 100644 index 0000000000..6fd7b1b02e --- /dev/null +++ b/awx/ui/test/unit/index.js @@ -0,0 +1,2 @@ +import './components'; + diff --git a/awx/ui/client/test/unit/karma.conf.js b/awx/ui/test/unit/karma.conf.js similarity index 100% rename from awx/ui/client/test/unit/karma.conf.js rename to awx/ui/test/unit/karma.conf.js diff --git a/awx/ui/test/unit/karma.unit.js b/awx/ui/test/unit/karma.unit.js new file mode 100644 index 0000000000..24a63c0cf9 --- /dev/null +++ b/awx/ui/test/unit/karma.unit.js @@ -0,0 +1,39 @@ +const path = require('path'); + +const SRC_PATH = path.resolve(__dirname, '../../client/src'); + +const webpackConfig = require('./webpack.unit'); + +module.exports = config => { + config.set({ + basePath: '', + singleRun: true, + autoWatch: false, + colors: true, + frameworks: ['jasmine'], + browsers: ['PhantomJS'], + reporters: ['progress'], + files: [ + path.join(SRC_PATH, 'vendor.js'), + path.join(SRC_PATH, 'app.js'), + path.join(SRC_PATH, '**/*.html'), + 'index.js' + ], + plugins: [ + 'karma-webpack', + 'karma-jasmine', + 'karma-phantomjs-launcher', + 'karma-html2js-preprocessor' + ], + preprocessors: { + [path.join(SRC_PATH, 'vendor.js')]: 'webpack', + [path.join(SRC_PATH, 'app.js')]: 'webpack', + [path.join(SRC_PATH, '**/*.html')]: 'html2js', + 'index.js': 'webpack' + }, + webpack: webpackConfig, + webpackMiddleware: { + noInfo: 'errors-only' + } + }); +}; diff --git a/awx/ui/client/test/unit/layout.spec.js b/awx/ui/test/unit/layout.spec.js similarity index 100% rename from awx/ui/client/test/unit/layout.spec.js rename to awx/ui/test/unit/layout.spec.js diff --git a/awx/ui/client/test/unit/side-nav-item.spec.js b/awx/ui/test/unit/side-nav-item.spec.js similarity index 100% rename from awx/ui/client/test/unit/side-nav-item.spec.js rename to awx/ui/test/unit/side-nav-item.spec.js diff --git a/awx/ui/client/test/unit/side-nav.spec.js b/awx/ui/test/unit/side-nav.spec.js similarity index 100% rename from awx/ui/client/test/unit/side-nav.spec.js rename to awx/ui/test/unit/side-nav.spec.js diff --git a/awx/ui/test/unit/webpack.unit.js b/awx/ui/test/unit/webpack.unit.js new file mode 100644 index 0000000000..7ec1a9a723 --- /dev/null +++ b/awx/ui/test/unit/webpack.unit.js @@ -0,0 +1,16 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const base = require('../../build/webpack.base'); + +const STATIC_URL = '/static/'; + +const test = { + devtool: 'cheap-source-map', + plugins: [ + new webpack.DefinePlugin({ + $basePath: STATIC_URL + }) + ] +}; + +module.exports = merge(base, test);