refactor: rewrite targets polling for ember-pollster

This commit is contained in:
Randall Morey committed 2022-01-02 19:40:45 -05:00
1 parent fe509d2e86
commit 71361799f7
2 files changed
+85 -142

No files matched your search

@@ -1,14 +1,13 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import config from '../../../../config/environment';
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 ipc;
@@ -23,26 +22,9 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
@resourceFilter({
allowed: (route) => route.modelFor('scopes.scope.projects'),
serialize: ({ id }) => id,
findBySerialized: ({ id }, value) => id === value
}) project;
/**
* A simple Ember Concurrency-based polling task that refreshes the route
* every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes
* to session `status` that may occur.
*
* NOTE: tasks are sort of attributes and sort of methods, but they are not
* language-level constructs. Thus we annotate this task as if it
* is an attribute.
* @type {Task}
*/
@task(function * () {
while(true) {
yield timeout(POLL_TIMEOUT_SECONDS * 1000);
yield this.refresh();
}
/* eslint-disable-next-line prettier/prettier */
}).drop() poller;
findBySerialized: ({ id }, value) => id === value,
})
project;
// =methods
@@ -61,33 +43,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
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
});
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,
}
);
}
/**
* When this route is activated (entered), begin polling for changes.
*/
activate() {
this.poller.perform();
}
/**
* When this route is deactivated (exited), stop polling for changes.
*/
deactivate() {
this.poller.cancelAll();
@runEvery(POLL_TIMEOUT_SECONDS * 1000)
poller() {
return this.refresh();
}
// =actions
@@ -112,7 +91,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
const options = {
target_id: model.id,
token: this.session.data.authenticated.token
token: this.session.data.authenticated.token,
};
if (host) options.host_id = host.id;
@@ -132,10 +111,11 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
session.proxy_address = address;
session.proxy_port = port;
if (credentials) {
credentials.forEach(cred => session.addCredential(cred));
credentials.forEach((cred) => session.addCredential(cred));
}
} catch(e) {
this.confirm.confirm(e.message, { isConnectError: true })
} catch (e) {
this.confirm
.confirm(e.message, { isConnectError: true })
// Retry
.then(() => this.connect(model))
.catch(() => null /* no op */);
@@ -9,7 +9,6 @@ import {
//getRootElement
//setupOnerror,
} from '@ember/test-helpers';
import { run, later } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs';
@@ -174,52 +173,35 @@ module('Acceptance | projects | targets', 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 () => {
run.cancelTimers();
await a11yAudit();
assert.equal(currentURL(), urls.targets);
assert.equal(findAll('tbody tr').length, targetsCount);
}, 750);
await visit(urls.targets);
await a11yAudit();
assert.equal(currentURL(), urls.targets);
assert.equal(findAll('tbody tr').length, targetsCount);
});
test('visiting a target', async function (assert) {
assert.expect(1);
later(async () => {
run.cancelTimers();
await click('tbody tr th a');
assert.equal(currentURL(), urls.sessions);
}, 750);
await visit(urls.targets);
await click('tbody tr th a');
assert.equal(currentURL(), urls.sessions);
});
test('visiting empty targets', async function (assert) {
assert.expect(1);
this.server.get('/targets', () => new Response(200));
later(async () => {
run.cancelTimers();
assert.equal(
find('.rose-message-title').textContent.trim(),
'No Targets Available'
);
}, 750);
await visit(urls.targets);
assert.equal(
find('.rose-message-title').textContent.trim(),
'No Targets Available'
);
});
test('cannot navigate to a target without proper authorization', async function (assert) {
assert.expect(1);
instances.target.authorized_actions =
instances.target.authorized_actions.filter((item) => item !== 'read');
later(async () => {
run.cancelTimers();
assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a'));
}, 750);
await visit(urls.targets);
assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a'));
});
test('connecting to a target', async function (assert) {
@@ -233,26 +215,22 @@ module('Acceptance | projects | targets', function (hooks) {
});
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
later(async () => {
run.cancelTimers();
await click(
'tbody tr:first-child td:last-child button',
'Activate connect mode'
);
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.equal(findAll('.rose-dialog-footer button').length, 1);
assert.equal(
find('.rose-dialog-footer button').textContent.trim(),
'Close',
'Cannot retry'
);
assert.equal(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
}, 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.equal(findAll('.rose-dialog-footer button').length, 1);
assert.equal(
find('.rose-dialog-footer button').textContent.trim(),
'Close',
'Cannot retry'
);
assert.equal(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
});
// Skipping because this test doesn't make sense. Users will never even see
@@ -269,12 +247,9 @@ module('Acceptance | projects | targets', function (hooks) {
assert.notOk(
instances.target.authorized_actions.includes('authorize-session')
);
later(async () => {
run.cancelTimers();
assert.ok(find('tbody tr:first-child'));
assert.notOk(find('tbody tr:first-child td:last-child button'));
}, 750);
await visit(urls.targets);
assert.ok(find('tbody tr:first-child'));
assert.notOk(find('tbody tr:first-child td:last-child button'));
});
test('handles cli error on connect', async function (assert) {
@@ -282,20 +257,16 @@ module('Acceptance | projects | targets', function (hooks) {
stubs.ipcService.withArgs('cliExists').returns(true);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
later(async () => {
run.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.equal(dialogButtons.length, 2);
assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
assert.equal(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.equal(dialogButtons.length, 2);
assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
});
test('handles connect error', async function (assert) {
@@ -304,20 +275,16 @@ module('Acceptance | projects | targets', function (hooks) {
stubs.ipcService.withArgs('connect').rejects();
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
later(async () => {
run.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.equal(dialogButtons.length, 2);
assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
assert.equal(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.equal(dialogButtons.length, 2);
assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
});
test('can retry on error', async function (assert) {
@@ -325,18 +292,14 @@ module('Acceptance | projects | targets', function (hooks) {
stubs.ipcService.withArgs('cliExists').rejects();
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
later(async () => {
run.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.targets);
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);
});
});