From 863c080196567e98841555d3b7c6b91a84b5825c Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 7 Jun 2023 18:28:33 -0700 Subject: [PATCH] Optimise Desktop API queries (#1707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 increase auth form width, which was too narrow * chore: 🤖 optimize targets route queries * chore: 🤖 optimize sessions route queries * chore: 🤖 disable polling in sessions route * chore: 🤖 disable polling in targets route * refactor: 💡 update tests for new queries and removing timing --- addons/api/mirage/factories/session.js | 14 +- addons/api/mirage/scenarios/default.js | 3 + .../routes/scopes/scope/projects/sessions.js | 54 ++-- .../routes/scopes/scope/projects/targets.js | 55 ++-- .../acceptance/projects/targets/hosts-test.js | 149 ++++------ .../projects/targets/sessions-test.js | 267 ++++++++++-------- ui/desktop/tests/acceptance/scopes-test.js | 170 +++++------ ui/desktop/tests/acceptance/targets-test.js | 12 +- 8 files changed, 335 insertions(+), 389 deletions(-) diff --git a/addons/api/mirage/factories/session.js b/addons/api/mirage/factories/session.js index 8c86ed601..7c6115a80 100644 --- a/addons/api/mirage/factories/session.js +++ b/addons/api/mirage/factories/session.js @@ -26,12 +26,18 @@ export default factory.extend({ withAssociations: trait({ afterCreate(record, server) { const scopeId = record.scopeId; - if (record.scope.type === 'project') { - const orgScopeId = record.scope?.scope.id; - const user = server.schema.users.where({ scopeId: orgScopeId }) - .models[0]; + + // Assign a user to this session if not already assigned. + if (record.scope && !record.user) { + const scopeId = + record.scope.type === 'project' + ? record.scope?.scope.id + : record.scope.id; + const user = server.schema.users.where({ scopeId }).models[0]; if (user) record.update({ user }); } + + // Assign target and host set to this session, if not already assigned. const target = server.schema.targets.where({ scopeId }).models[0]; if (target) { record.update({ target }); diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js index 17a4fd90c..dd577e84b 100644 --- a/addons/api/mirage/scenarios/default.js +++ b/addons/api/mirage/scenarios/default.js @@ -102,6 +102,9 @@ export default function (server) { server.create('target', { scope, address: '0.0.0.0' }); // Sessions have target data. Create it after targets. server.createList('session', 4, { scope }, 'withAssociations'); + // Also create sessions for "authenticateduser" for Desktop, + // which filters user_id by the currently authenticated user. + server.createList('session', 1, { scope, user }, 'withAssociations'); // Create IAM resources for project server.createList('group', 3, { scope }); server.createList('role', 3, { scope }); diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/sessions.js index 40e33e13b..a9e0032d7 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions.js @@ -6,12 +6,8 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -import runEvery from 'ember-pollster/decorators/route/run-every'; import { notifySuccess, notifyError } from 'core/decorators/notify'; import { resourceFilter } from 'core/decorators/resource-filter'; -import config from '../../../../config/environment'; - -const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds; export default class ScopesScopeProjectsSessionsRoute extends Route { // =services @@ -47,32 +43,42 @@ export default class ScopesScopeProjectsSessionsRoute extends Route { } /** - * Loads all sessions under the current scope and encapsulates them into - * an array of objects filtering to current user - * @return {Promise{SessionModel[]}} + * Loads all sessions under current scope for the current user. + * + * NOTE: previously, sessions were filtered only with API filter queries. + * In an effort to offload processing from the controller, sessions + * are now filtered on the client by projects and status, + * while user_id filtering remains server side. + * + * @return {Promise{[SessionModel]}} */ async model() { - const { status } = this; const { id: scope_id } = this.modelFor('scopes.scope'); - const { user_id } = this.session.data.authenticated; - const projects = this.project || []; - const filters = { - user_id, - status, - scope_id: projects.map(({ id }) => id), - }; - const options = { - recursive: true, + const queryOptions = { scope_id, - include_terminated: filters.status?.includes('terminated'), + recursive: true, + include_terminated: this.status?.includes('terminated'), }; - await this.store.query('target', { recursive: true, scope_id }); - return await this.resourceFilterStore.queryBy('session', filters, options); - } - @runEvery(POLL_TIMEOUT_SECONDS * 1000) - poller() { - return this.refresh(); + // Recursively query sessions within the current scope for the current user + let sessions = await this.resourceFilterStore.queryBy( + 'session', + { user_id: this.session.data.authenticated.user_id }, + queryOptions + ); + + // If project filters are selected, filter sessions by the selected projects + if (this.project?.length) { + const projectIDs = this.project.map((p) => p?.id); + sessions = sessions.filter((s) => projectIDs.includes(s?.scopeID)); + } + + // If status filters are selected, filter sessions by the selected statuses + if (this.status?.length) { + sessions = sessions.filter((s) => this.status.includes(s?.status)); + } + + return sessions; } // =actions diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 43bc53634..ebb95c836 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -7,21 +7,18 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { resourceFilter } from 'core/decorators/resource-filter'; -import runEvery from 'ember-pollster/decorators/route/run-every'; -import config from '../../../../config/environment'; - -const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds; export default class ScopesScopeProjectsTargetsRoute extends Route { // =services - @service store; - @service ipc; - @service session; + @service can; @service clusterUrl; @service confirm; + @service ipc; @service resourceFilterStore; @service router; + @service session; + @service store; // =attributes @@ -43,36 +40,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { /** * Loads all targets under current scope. + * + * NOTE: previously, targets were filtered with API filter queries. + * In an effort to offload processing from the controller, targets + * are now filtered on the client by projects and authorized_actions. + * * @return {Promise{[TargetModel]}} */ async model() { const { id: scope_id } = this.modelFor('scopes.scope'); - const { user_id } = this.session.data.authenticated; - const projects = this.project || []; - await this.resourceFilterStore.queryBy( - 'session', - { user_id }, - { - recursive: true, - scope_id, - } - ); - return this.resourceFilterStore.queryBy( - 'target', - { - scope_id: projects.map(({ id }) => id), - authorized_actions: [{ contains: 'authorize-session' }], - }, - { - recursive: true, - scope_id, - } - ); - } + const queryOptions = { scope_id, recursive: true }; - @runEvery(POLL_TIMEOUT_SECONDS * 1000) - poller() { - return this.refresh(); + // Recursively query all targets within the current scope + let targets = await this.store.query('target', queryOptions); + + // Filter out targets to which users do not have the connect ability + targets = targets.filter((t) => this.can.can('connect target', t)); + + // If project filters are selected, filter targets by the selected projects + if (this.project?.length) { + const projectIDs = this.project.map((p) => p?.id); + targets = targets.filter((t) => projectIDs.includes(t?.scopeID)); + } + + return targets; } // =actions diff --git a/ui/desktop/tests/acceptance/projects/targets/hosts-test.js b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js index 49c3696b0..b65f6adb2 100644 --- a/ui/desktop/tests/acceptance/projects/targets/hosts-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js @@ -5,7 +5,6 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; -import { later, _cancelTimers } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -169,31 +168,19 @@ module('Acceptance | projects | targets | hosts', function (hooks) { test('visiting index', async function (assert) { assert.expect(2); const hostsCount = this.server.schema.hosts.all().models.length; - // This later/cancelTimers technique allows us to test a page with - // active polling. Normally an acceptance test waits for all runloop timers - // to stop before returning from an awaited test, but polling means that - // runloop timers exist indefinitely. We thus schedule a cancelation before - // proceeding with our tests. - later(async () => { - _cancelTimers(); - // await a11yAudit(); - assert.strictEqual(currentURL(), urls.hosts); - assert.strictEqual(findAll('tbody tr').length, hostsCount); - }, 750); await visit(urls.hosts); + assert.strictEqual(currentURL(), urls.hosts); + assert.strictEqual(findAll('tbody tr').length, hostsCount); }); test('visiting empty hosts', async function (assert) { assert.expect(1); instances.target.update({ hostSets: [] }); - later(async () => { - _cancelTimers(); - assert.ok( - find('.rose-message-title').textContent.trim(), - 'No Hosts Available' - ); - }, 750); await visit(urls.hosts); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Hosts Available' + ); }); test('connecting to a host', async function (assert) { @@ -208,24 +195,21 @@ module('Acceptance | projects | targets | hosts', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.dialog-detail'), 'Success dialog'); - assert.strictEqual( - find('.rose-dialog-footer .rose-button-secondary').textContent.trim(), - 'Close', - 'Cannot retry' - ); - assert.strictEqual( - find('.rose-dialog-body .copyable-content').textContent.trim(), - 'a_123:p_123' - ); - }, 750); await visit(urls.hosts); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.dialog-detail'), 'Success dialog'); + assert.strictEqual( + find('.rose-dialog-footer .rose-button-secondary').textContent.trim(), + 'Close', + 'Cannot retry' + ); + assert.strictEqual( + find('.rose-dialog-body .copyable-content').textContent.trim(), + 'a_123:p_123' + ); }); test('handles cli error on connect', async function (assert) { @@ -234,27 +218,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.hosts); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); test('handles connect error', async function (assert) { @@ -264,27 +245,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.hosts); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); test('can retry on error', async function (assert) { @@ -293,17 +271,14 @@ module('Acceptance | projects | targets | hosts', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - const firstErrorDialog = find('.rose-dialog'); - await click('.rose-dialog footer .rose-button-primary', 'Retry'); - const secondErrorDialog = find('.rose-dialog'); - assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); - }, 750); await visit(urls.hosts); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + const firstErrorDialog = find('.rose-dialog'); + await click('.rose-dialog footer .rose-button-primary', 'Retry'); + const secondErrorDialog = find('.rose-dialog'); + assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); }); }); diff --git a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js index 4cdae3a51..ffc460e88 100644 --- a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js @@ -5,7 +5,6 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; -import { later, _cancelTimers } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -173,107 +172,140 @@ module('Acceptance | projects | targets | sessions', function (hooks) { }); test('visiting index', async function (assert) { - assert.expect(2); - const sessionsCount = this.server.schema.sessions.all().models.length; - // This later/cancelTimers technique allows us to test a page with - // active polling. Normally an acceptance test waits for all runloop timers - // to stop before returning from an awaited test, but polling means that - // runloop timers exist indefinitely. We thus schedule a cancelation before - // proceeding with our tests. - later(async () => { - _cancelTimers(); - // await a11yAudit(); - assert.strictEqual(currentURL(), urls.sessions); - assert.strictEqual(findAll('tbody tr').length, sessionsCount); - }, 750); + assert.expect(1); await visit(urls.sessions); + assert.strictEqual(currentURL(), urls.sessions); }); test('visiting index redirects to sessions', async function (assert) { assert.expect(1); - later(async () => { - _cancelTimers(); - // await a11yAudit(); - assert.strictEqual(currentURL(), urls.sessions); - }, 750); await visit(urls.target); + assert.strictEqual(currentURL(), urls.sessions); }); test('visiting empty sessions', async function (assert) { assert.expect(1); this.server.get('/sessions', () => new Response(200)); - later(async () => { - _cancelTimers(); - assert.ok( - find('.rose-message-title').textContent.trim(), - 'No Sessions Available' - ); - }, 750); await visit(urls.sessions); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Sessions Available' + ); }); test('can identify target with active sessions', async function (assert) { assert.expect(1); - later(async () => { - _cancelTimers(); - assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); - }, 750); + + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + await click('.rose-dialog-footer .rose-button-secondary'); + assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); }); test('can identify target with pending sessions', async function (assert) { assert.expect(1); + instances.session.update({ status: 'pending' }); - later(async () => { - _cancelTimers(); - assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); - }, 750); + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + await click('.rose-dialog-footer .rose-button-secondary'); + assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); }); test('cannot identify target with terminated sessions', async function (assert) { assert.expect(1); instances.session.update({ status: 'terminated' }); - later(async () => { - _cancelTimers(); - assert.notOk(find('.rose-layout-page-header .hds-badge--color-success')); - }, 750); + await visit(urls.sessions); + assert.notOk(find('.rose-layout-page-header .hds-badge--color-success')); }); test('cancelling a session', async function (assert) { assert.expect(2); - const sessionsCount = this.server.schema.sessions.all().models.length; - stubs.ipcService.withArgs('cancel'); - later(async () => { - _cancelTimers(); - await click('tbody tr:first-child td:last-child button'); - assert.ok(find('[role="alert"].is-success')); - assert.strictEqual(findAll('tbody tr').length, sessionsCount - 1); - }, 750); + + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + await click('.rose-dialog-footer .rose-button-secondary'); + const sessionsCount = findAll( + 'tbody tr:first-child td:last-child button' + ).length; + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-success')); + assert.strictEqual(findAll('tbody tr').length, sessionsCount - 1); }); test('cancelling a session with error shows notification', async function (assert) { assert.expect(1); + + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + this.server.post('/sessions/:id_method', () => new Response(400)); - later(async () => { - _cancelTimers(); - await click('tbody tr:first-child td:last-child button'); - assert.ok(find('[role="alert"].is-error')); - }, 750); + await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + await click('.rose-dialog-footer .rose-button-secondary'); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-error')); }); test('cancelling a session with ipc error shows notification', async function (assert) { assert.expect(1); + + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + stubs.ipcService.withArgs('stop').throws(); - later(async () => { - _cancelTimers(); - await click('tbody tr:first-child td:last-child button'); - assert.ok(find('[role="alert"].is-error')); - }, 750); + await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + await click('.rose-dialog-footer .rose-button-secondary'); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-error')); }); test('connecting to a target', async function (assert) { @@ -288,21 +320,18 @@ module('Acceptance | projects | targets | sessions', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click('.rose-layout-page-actions button', 'Activate connect mode'); - assert.ok(find('.dialog-detail'), 'Success dialog'); - assert.strictEqual( - find('.rose-dialog-footer .rose-button-secondary').textContent.trim(), - 'Close', - 'Cannot retry' - ); - assert.strictEqual( - find('.rose-dialog-body .copyable-content').textContent.trim(), - 'a_123:p_123' - ); - }, 750); await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + assert.ok(find('.dialog-detail'), 'Success dialog'); + assert.strictEqual( + find('.rose-dialog-footer .rose-button-secondary').textContent.trim(), + 'Close', + 'Cannot retry' + ); + assert.strictEqual( + find('.rose-dialog-body .copyable-content').textContent.trim(), + 'a_123:p_123' + ); }); test('connect details in session', async function (assert) { @@ -317,20 +346,17 @@ module('Acceptance | projects | targets | sessions', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - instances.session.update('status', 'active'); - await click('.rose-layout-page-actions button', 'Activate connect mode'); - assert.ok(find('.dialog-detail'), 'Success dialog'); - await click('.rose-dialog-dismiss'); - assert.strictEqual( - find( - 'tbody tr:first-child td:nth-child(2) .copyable-content' - ).textContent.trim(), - 'a_123:p_123' - ); - }, 750); await visit(urls.sessions); + instances.session.update('status', 'active'); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + assert.ok(find('.dialog-detail'), 'Success dialog'); + await click('.rose-dialog-dismiss'); + assert.strictEqual( + find( + 'tbody tr:first-child td:nth-child(2) .copyable-content' + ).textContent.trim(), + 'a_123:p_123' + ); }); test('handles cli error on connect', async function (assert) { @@ -339,24 +365,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click('.rose-layout-page-actions button', 'Activate connect mode'); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); test('handles connect error', async function (assert) { @@ -366,24 +389,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click('.rose-layout-page-actions button', 'Activate connect mode'); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); test('can retry on error', async function (assert) { @@ -392,14 +412,11 @@ module('Acceptance | projects | targets | sessions', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click('.rose-layout-page-actions button', 'Activate connect mode'); - const firstErrorDialog = find('.rose-dialog'); - await click('.rose-dialog footer .rose-button-primary', 'Retry'); - const secondErrorDialog = find('.rose-dialog'); - assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); - }, 750); await visit(urls.sessions); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + const firstErrorDialog = find('.rose-dialog'); + await click('.rose-dialog footer .rose-button-primary', 'Retry'); + const secondErrorDialog = find('.rose-dialog'); + assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); }); }); diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js index 5d539f442..1a8c2970f 100644 --- a/ui/desktop/tests/acceptance/scopes-test.js +++ b/ui/desktop/tests/acceptance/scopes-test.js @@ -5,7 +5,6 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; -import { later, _cancelTimers } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -173,28 +172,17 @@ module('Acceptance | scopes', function (hooks) { test('visiting index', async function (assert) { assert.expect(2); const targetsCount = this.server.schema.targets.all().models.length; - // This later/cancelTimers technique allows us to test a page with - // active polling. Normally an acceptance test waits for all runloop timers - // to stop before returning from an awaited test, but polling means that - // runloop timers exist indefinitely. We thus schedule a cancelation before - // proceeding with our tests. - later(async () => { - _cancelTimers(); - await a11yAudit(); - assert.strictEqual(currentURL(), urls.targets); - assert.strictEqual(findAll('tbody tr').length, targetsCount); - }, 750); + await visit(urls.targets); + assert.strictEqual(currentURL(), urls.targets); + assert.strictEqual(findAll('tbody tr').length, targetsCount); }); test('visiting global scope', async function (assert) { assert.expect(1); - later(async () => { - _cancelTimers(); - await a11yAudit(); - assert.strictEqual(currentURL(), urls.globalTargets); - }, 750); await visit(urls.scopes.global); + await a11yAudit(); + assert.strictEqual(currentURL(), urls.globalTargets); }); // TODO: this probably shouldn't be the case, but was setup to enable @@ -208,43 +196,28 @@ module('Acceptance | scopes', function (hooks) { const response = id === 'global' ? new Response(404) : scope; return response; }); - later(async () => { - _cancelTimers(); - await a11yAudit(); - assert.strictEqual(currentURL(), urls.globalTargets); - }, 750); await visit(urls.scopes.global); + await a11yAudit(); + assert.strictEqual(currentURL(), urls.globalTargets); }); test('visiting org scope', async function (assert) { assert.expect(1); - later(async () => { - _cancelTimers(); - await a11yAudit(); - assert.strictEqual(currentURL(), urls.targets); - }, 750); await visit(urls.scopes.org); + await a11yAudit(); + assert.strictEqual(currentURL(), urls.targets); }); test('can navigate among org scopes via header navigation', async function (assert) { assert.expect(3); - await later(async () => _cancelTimers(), 750); await visit(urls.targets); - await later( - async () => assert.strictEqual(currentURL(), urls.targets), - 750 - ); + await click('.rose-header-nav .rose-dropdown a:nth-of-type(2)'); - await later( - async () => assert.strictEqual(currentURL(), urls.org2Targets), - 750 - ); + assert.strictEqual(currentURL(), urls.targets); await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)'); - later( - async () => assert.strictEqual(currentURL(), urls.globalTargets), - 750 - ); + assert.strictEqual(currentURL(), urls.org2Targets); await click('.rose-header-nav .rose-dropdown a:nth-of-type(1)'); + assert.strictEqual(currentURL(), urls.globalTargets); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { @@ -258,25 +231,19 @@ module('Acceptance | scopes', function (hooks) { test('visiting a target', async function (assert) { assert.expect(1); - later(async () => { - _cancelTimers(); - await click('tbody tr th a'); - assert.strictEqual(currentURL(), urls.targetSessions); - }, 750); await visit(urls.targets); + await click('tbody tr th a'); + assert.strictEqual(currentURL(), urls.targetSessions); }); test('visiting empty targets', async function (assert) { assert.expect(1); this.server.get('/targets', () => new Response(200)); - later(async () => { - _cancelTimers(); - assert.ok( - find('.rose-message-title').textContent.trim(), - 'No Targets Available' - ); - }, 750); await visit(urls.targets); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Targets Available' + ); }); test('connecting to a target', async function (assert) { @@ -291,21 +258,18 @@ module('Acceptance | scopes', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.dialog-detail'), 'Success dialog'); - assert.strictEqual(findAll('.rose-dialog-footer button').length, 1); - assert.strictEqual( - find('.rose-dialog-footer button').textContent.trim(), - 'Close', - 'Cannot retry' - ); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.dialog-detail'), 'Success dialog'); + assert.strictEqual(findAll('.rose-dialog-footer button').length, 1); + assert.strictEqual( + find('.rose-dialog-footer button').textContent.trim(), + 'Close', + 'Cannot retry' + ); }); test('handles cli error on connect', async function (assert) { @@ -314,27 +278,24 @@ module('Acceptance | scopes', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.scopes.global); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); test('handles connect error', async function (assert) { @@ -344,26 +305,23 @@ module('Acceptance | scopes', function (hooks) { const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; - later(async () => { - _cancelTimers(); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); - assert.ok(find('.rose-dialog-error'), 'Error dialog'); - const dialogButtons = findAll('.rose-dialog-footer button'); - assert.strictEqual(dialogButtons.length, 2); - assert.strictEqual( - dialogButtons[0].textContent.trim(), - 'Retry', - 'Can retry' - ); - assert.strictEqual( - dialogButtons[1].textContent.trim(), - 'Cancel', - 'Can cancel' - ); - }, 750); await visit(urls.targets); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.strictEqual(dialogButtons.length, 2); + assert.strictEqual( + dialogButtons[0].textContent.trim(), + 'Retry', + 'Can retry' + ); + assert.strictEqual( + dialogButtons[1].textContent.trim(), + 'Cancel', + 'Can cancel' + ); }); }); diff --git a/ui/desktop/tests/acceptance/targets-test.js b/ui/desktop/tests/acceptance/targets-test.js index 17a2ca990..2ea8a5678 100644 --- a/ui/desktop/tests/acceptance/targets-test.js +++ b/ui/desktop/tests/acceptance/targets-test.js @@ -5,7 +5,6 @@ import { module, test } from 'qunit'; import { visit, currentURL } from '@ember/test-helpers'; -import { later, _cancelTimers } from '@ember/runloop'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; @@ -113,16 +112,7 @@ module('Acceptance | targets', function (hooks) { test('visiting index', async function (assert) { assert.expect(1); - // This later/cancelTimers technique allows us to test a page with - // active polling. Normally an acceptance test waits for all runloop timers - // to stop before returning from an awaited test, but polling means that - // runloop timers exist indefinitely. We thus schedule a cancellation before - // proceeding with our tests. - later(async () => { - _cancelTimers(); - await a11yAudit(); - assert.strictEqual(currentURL(), urls.targets); - }, 750); await visit(urls.targets); + assert.strictEqual(currentURL(), urls.targets); }); });