open-nomad/ui/tests/acceptance/job-deployments-test.js
Jai Bhagat 3350f3fb11 ui: apply new qunit linting rules to tests
Async tests should use  in integrations tests.
Acceptance tests are using Mirage and can't use
since we can't know the number of assertions.
2022-01-20 10:01:35 -05:00

317 lines
9.3 KiB
JavaScript

import { currentURL } from '@ember/test-helpers';
import { get } from '@ember/object';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import moment from 'moment';
import Deployments from 'nomad-ui/tests/pages/jobs/job/deployments';
const sum = (list, key, getter = (a) => a) =>
list.reduce((sum, item) => sum + getter(get(item, key)), 0);
let job;
let deployments;
let sortedDeployments;
module('Acceptance | job deployments', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
server.create('node');
job = server.create('job');
deployments = server.schema.deployments.where({ jobId: job.id });
sortedDeployments = deployments.sort((a, b) => {
const aVersion = server.db.jobVersions.findBy({
jobId: a.jobId,
version: a.versionNumber,
});
const bVersion = server.db.jobVersions.findBy({
jobId: b.jobId,
version: b.versionNumber,
});
if (aVersion.submitTime < bVersion.submitTime) {
return 1;
} else if (aVersion.submitTime > bVersion.submitTime) {
return -1;
}
return 0;
});
});
test('it passes an accessibility audit', async function (assert) {
assert.expect(1);
await Deployments.visit({ id: job.id });
await a11yAudit(assert);
});
test('/jobs/:id/deployments should list all job deployments', async function (assert) {
await Deployments.visit({ id: job.id });
assert.equal(
Deployments.deployments.length,
deployments.length,
'Each deployment gets a row in the timeline'
);
assert.equal(document.title, `Job ${job.name} deployments - Nomad`);
});
test('each deployment mentions the deployment shortId, status, version, and time since it was submitted', async function (assert) {
await Deployments.visit({ id: job.id });
const deployment = sortedDeployments.models[0];
const version = server.db.jobVersions.findBy({
jobId: deployment.jobId,
version: deployment.versionNumber,
});
const deploymentRow = Deployments.deployments.objectAt(0);
assert.ok(
deploymentRow.text.includes(deployment.id.split('-')[0]),
'Short ID'
);
assert.equal(deploymentRow.status, deployment.status, 'Status');
assert.ok(
deploymentRow.statusClass.includes(classForStatus(deployment.status)),
'Status Class'
);
assert.ok(
deploymentRow.version.includes(deployment.versionNumber),
'Version #'
);
assert.ok(
deploymentRow.submitTime.includes(
moment(version.submitTime / 1000000).fromNow()
),
'Submit time ago'
);
});
test('when the deployment is running and needs promotion, the deployment item says so', async function (assert) {
// Ensure the deployment needs deployment
const deployment = sortedDeployments.models[0];
const taskGroupSummary = deployment.deploymentTaskGroupSummaryIds.map(
(id) => server.schema.deploymentTaskGroupSummaries.find(id)
)[0];
deployment.update('status', 'running');
deployment.save();
taskGroupSummary.update({
desiredCanaries: 1,
placedCanaries: [],
promoted: false,
});
taskGroupSummary.save();
await Deployments.visit({ id: job.id });
const deploymentRow = Deployments.deployments.objectAt(0);
assert.ok(
deploymentRow.promotionIsRequired,
'Requires Promotion badge found'
);
});
test('each deployment item can be opened to show details', async function (assert) {
await Deployments.visit({ id: job.id });
const deploymentRow = Deployments.deployments.objectAt(0);
assert.notOk(deploymentRow.hasDetails, 'No deployment body');
await deploymentRow.toggle();
assert.ok(deploymentRow.hasDetails, 'Deployment body found');
});
test('when open, a deployment shows the deployment metrics', async function (assert) {
await Deployments.visit({ id: job.id });
const deployment = sortedDeployments.models[0];
const deploymentRow = Deployments.deployments.objectAt(0);
const taskGroupSummaries = deployment.deploymentTaskGroupSummaryIds.map(
(id) => server.db.deploymentTaskGroupSummaries.find(id)
);
await deploymentRow.toggle();
assert.equal(
deploymentRow.metricFor('canaries').text,
`${sum(taskGroupSummaries, 'placedCanaries', (a) => a.length)} / ${sum(
taskGroupSummaries,
'desiredCanaries'
)}`,
'Canaries, both places and desired, are in the metrics'
);
assert.equal(
deploymentRow.metricFor('placed').text,
sum(taskGroupSummaries, 'placedAllocs'),
'Placed allocs aggregates across task groups'
);
assert.equal(
deploymentRow.metricFor('desired').text,
sum(taskGroupSummaries, 'desiredTotal'),
'Desired allocs aggregates across task groups'
);
assert.equal(
deploymentRow.metricFor('healthy').text,
sum(taskGroupSummaries, 'healthyAllocs'),
'Healthy allocs aggregates across task groups'
);
assert.equal(
deploymentRow.metricFor('unhealthy').text,
sum(taskGroupSummaries, 'unhealthyAllocs'),
'Unhealthy allocs aggregates across task groups'
);
assert.equal(
deploymentRow.notification,
deployment.statusDescription,
'Status description is in the metrics block'
);
});
test('when open, a deployment shows a list of all task groups and their respective stats', async function (assert) {
await Deployments.visit({ id: job.id });
const deployment = sortedDeployments.models[0];
const deploymentRow = Deployments.deployments.objectAt(0);
const taskGroupSummaries = deployment.deploymentTaskGroupSummaryIds.map(
(id) => server.db.deploymentTaskGroupSummaries.find(id)
);
await deploymentRow.toggle();
assert.ok(deploymentRow.hasTaskGroups, 'Task groups found');
assert.equal(
deploymentRow.taskGroups.length,
taskGroupSummaries.length,
'One row per task group'
);
const taskGroup = taskGroupSummaries[0];
const taskGroupRow = deploymentRow.taskGroups.findOneBy(
'name',
taskGroup.name
);
assert.equal(taskGroupRow.name, taskGroup.name, 'Name');
assert.equal(
taskGroupRow.promotion,
promotionTestForTaskGroup(taskGroup),
'Needs Promotion'
);
assert.equal(
taskGroupRow.autoRevert,
taskGroup.autoRevert ? 'Yes' : 'No',
'Auto Revert'
);
assert.equal(
taskGroupRow.canaries,
`${taskGroup.placedCanaries.length} / ${taskGroup.desiredCanaries}`,
'Canaries'
);
assert.equal(
taskGroupRow.allocs,
`${taskGroup.placedAllocs} / ${taskGroup.desiredTotal}`,
'Allocs'
);
assert.equal(
taskGroupRow.healthy,
taskGroup.healthyAllocs,
'Healthy Allocs'
);
assert.equal(
taskGroupRow.unhealthy,
taskGroup.unhealthyAllocs,
'Unhealthy Allocs'
);
assert.equal(
taskGroupRow.progress,
moment(taskGroup.requireProgressBy).format("MMM DD, 'YY HH:mm:ss ZZ"),
'Progress By'
);
});
test('when open, a deployment shows a list of all allocations for the deployment', async function (assert) {
await Deployments.visit({ id: job.id });
const deployment = sortedDeployments.models[0];
const deploymentRow = Deployments.deployments.objectAt(0);
// TODO: Make this less brittle. This logic is copied from the mirage config,
// since there is no reference to allocations on the deployment model.
const allocations = server.db.allocations
.where({ jobId: deployment.jobId })
.slice(0, 3);
await deploymentRow.toggle();
assert.ok(deploymentRow.hasAllocations, 'Allocations found');
assert.equal(
deploymentRow.allocations.length,
allocations.length,
'One row per allocation'
);
const allocation = allocations[0];
const allocationRow = deploymentRow.allocations.objectAt(0);
assert.equal(
allocationRow.shortId,
allocation.id.split('-')[0],
'Allocation is as expected'
);
});
test('when the job for the deployments is not found, an error message is shown, but the URL persists', async function (assert) {
await Deployments.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests
.filter((request) => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);
assert.equal(
currentURL(),
'/jobs/not-a-real-job/deployments',
'The URL persists'
);
assert.ok(Deployments.error.isPresent, 'Error message is shown');
assert.equal(
Deployments.error.title,
'Not Found',
'Error message is for 404'
);
});
function classForStatus(status) {
const classMap = {
running: 'is-running',
successful: 'is-primary',
paused: 'is-light',
failed: 'is-error',
cancelled: 'is-cancelled',
};
return classMap[status] || 'is-dark';
}
function promotionTestForTaskGroup(taskGroup) {
if (taskGroup.desiredCanaries > 0 && taskGroup.promoted === false) {
return 'Yes';
} else if (taskGroup.desiredCanaries > 0) {
return 'No';
}
return 'N/A';
}
});