open-nomad/ui/tests/acceptance/job-detail-test.js

452 lines
14 KiB
JavaScript

import { get } from '@ember/object';
import $ from 'jquery';
import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers';
import moment from 'moment';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
let job;
moduleForAcceptance('Acceptance | job detail', {
beforeEach() {
server.create('node');
job = server.create('job', { type: 'service' });
visit(`/jobs/${job.id}`);
},
});
test('visiting /jobs/:job_id', function(assert) {
assert.equal(currentURL(), `/jobs/${job.id}`);
});
test('breadcrumbs includes job name and link back to the jobs list', function(assert) {
assert.equal(findAll('.breadcrumb a')[0].textContent, 'Jobs', 'First breadcrumb says jobs');
assert.equal(
findAll('.breadcrumb a')[1].textContent,
job.name,
'Second breadcrumb says the job name'
);
click(findAll('.breadcrumb a')[0]);
andThen(() => {
assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs');
});
});
test('the subnav includes links to definition, versions, and deployments when type = service', function(
assert
) {
const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent);
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
});
test('the subnav includes links to definition and versions when type != service', function(assert) {
job = server.create('job', { type: 'batch' });
visit(`/jobs/${job.id}`);
andThen(() => {
const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent);
assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
});
});
test('the job detail page should contain basic information about the job', function(assert) {
assert.ok(findAll('.title .tag')[0].textContent.includes(job.status), 'Status');
assert.ok(findAll('.job-stats span')[0].textContent.includes(job.type), 'Type');
assert.ok(findAll('.job-stats span')[1].textContent.includes(job.priority), 'Priority');
assert.notOk(findAll('.job-stats span')[2], 'Namespace is not included');
});
test('the job detail page should list all task groups', function(assert) {
assert.equal(
findAll('.task-group-row').length,
server.db.taskGroups.where({ jobId: job.id }).length
);
});
test('each row in the task group table should show basic information about the task group', function(
assert
) {
const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0];
const taskGroupRow = $(findAll('.task-group-row')[0]);
const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id });
const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
assert.equal(
taskGroupRow
.find('td:eq(0)')
.text()
.trim(),
taskGroup.name,
'Name'
);
assert.equal(
taskGroupRow
.find('td:eq(1)')
.text()
.trim(),
taskGroup.count,
'Count'
);
assert.equal(
taskGroupRow.find('td:eq(3)').text(),
`${sum(tasks, 'Resources.CPU')} MHz`,
'Reserved CPU'
);
assert.equal(
taskGroupRow.find('td:eq(4)').text(),
`${sum(tasks, 'Resources.MemoryMB')} MiB`,
'Reserved Memory'
);
assert.equal(
taskGroupRow.find('td:eq(5)').text(),
`${taskGroup.ephemeralDisk.SizeMB} MiB`,
'Reserved Disk'
);
});
test('the allocations diagram lists all allocation status figures', function(assert) {
const legend = find('.distribution-bar .legend');
const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id });
const statusCounts = Object.keys(jobSummary.Summary).reduce(
(counts, key) => {
const group = jobSummary.Summary[key];
counts.queued += group.Queued;
counts.starting += group.Starting;
counts.running += group.Running;
counts.complete += group.Complete;
counts.failed += group.Failed;
counts.lost += group.Lost;
return counts;
},
{ queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 }
);
assert.equal(
legend.querySelector('li.queued .value').textContent,
statusCounts.queued,
`${statusCounts.queued} are queued`
);
assert.equal(
legend.querySelector('li.starting .value').textContent,
statusCounts.starting,
`${statusCounts.starting} are starting`
);
assert.equal(
legend.querySelector('li.running .value').textContent,
statusCounts.running,
`${statusCounts.running} are running`
);
assert.equal(
legend.querySelector('li.complete .value').textContent,
statusCounts.complete,
`${statusCounts.complete} are complete`
);
assert.equal(
legend.querySelector('li.failed .value').textContent,
statusCounts.failed,
`${statusCounts.failed} are failed`
);
assert.equal(
legend.querySelector('li.lost .value').textContent,
statusCounts.lost,
`${statusCounts.lost} are lost`
);
});
test('there is no active deployment section when the job has no active deployment', function(
assert
) {
// TODO: it would be better to not visit two different job pages in one test, but this
// way is much more convenient.
job = server.create('job', { noActiveDeployment: true, type: 'service' });
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(findAll('.active-deployment').length === 0, 'No active deployment');
});
});
test('the active deployment section shows up for the currently running deployment', function(
assert
) {
job = server.create('job', { activeDeployment: true, type: 'service' });
const deployment = server.db.deployments.where({ jobId: job.id })[0];
const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({
deploymentId: deployment.id,
});
const version = server.db.jobVersions.findBy({
jobId: job.id,
version: deployment.versionNumber,
});
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(findAll('.active-deployment').length === 1, 'Active deployment');
assert.equal(
$('.active-deployment > .boxed-section-head .badge')
.get(0)
.textContent.trim(),
deployment.id.split('-')[0],
'The active deployment is the most recent running deployment'
);
assert.equal(
$('.active-deployment > .boxed-section-head .submit-time')
.get(0)
.textContent.trim(),
moment(version.submitTime / 1000000).fromNow(),
'Time since the job was submitted is in the active deployment header'
);
assert.equal(
$('.deployment-metrics .label:contains("Canaries") + .value')
.get(0)
.textContent.trim(),
`${sum(taskGroupSummaries, 'placedCanaries')} / ${sum(
taskGroupSummaries,
'desiredCanaries'
)}`,
'Canaries, both places and desired, are in the metrics'
);
assert.equal(
$('.deployment-metrics .label:contains("Placed") + .value')
.get(0)
.textContent.trim(),
sum(taskGroupSummaries, 'placedAllocs'),
'Placed allocs aggregates across task groups'
);
assert.equal(
$('.deployment-metrics .label:contains("Desired") + .value')
.get(0)
.textContent.trim(),
sum(taskGroupSummaries, 'desiredTotal'),
'Desired allocs aggregates across task groups'
);
assert.equal(
$('.deployment-metrics .label:contains("Healthy") + .value')
.get(0)
.textContent.trim(),
sum(taskGroupSummaries, 'healthyAllocs'),
'Healthy allocs aggregates across task groups'
);
assert.equal(
$('.deployment-metrics .label:contains("Unhealthy") + .value')
.get(0)
.textContent.trim(),
sum(taskGroupSummaries, 'unhealthyAllocs'),
'Unhealthy allocs aggregates across task groups'
);
assert.equal(
$('.deployment-metrics .notification')
.get(0)
.textContent.trim(),
deployment.statusDescription,
'Status description is in the metrics block'
);
});
});
test('the active deployment section can be expanded to show task groups and allocations', function(
assert
) {
job = server.create('job', { activeDeployment: true, type: 'service' });
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(
$('.active-deployment .boxed-section-head:contains("Task Groups")').length === 0,
'Task groups not found'
);
assert.ok(
$('.active-deployment .boxed-section-head:contains("Allocations")').length === 0,
'Allocations not found'
);
});
andThen(() => {
click('.active-deployment-details-toggle');
});
andThen(() => {
assert.ok(
$('.active-deployment .boxed-section-head:contains("Task Groups")').length === 1,
'Task groups found'
);
assert.ok(
$('.active-deployment .boxed-section-head:contains("Allocations")').length === 1,
'Allocations found'
);
});
});
test('the evaluations table lists evaluations sorted by modify index', function(assert) {
job = server.create('job');
const evaluations = server.db.evaluations
.where({ jobId: job.id })
.sortBy('modifyIndex')
.reverse();
visit(`/jobs/${job.id}`);
andThen(() => {
assert.equal(
findAll('.evaluations tbody tr').length,
evaluations.length,
'A row for each evaluation'
);
evaluations.forEach((evaluation, index) => {
const row = $(findAll('.evaluations tbody tr')[index]);
assert.equal(
row.find('td:eq(0)').text(),
evaluation.id.split('-')[0],
`Short ID, row ${index}`
);
});
const firstEvaluation = evaluations[0];
const row = $(findAll('.evaluations tbody tr')[0]);
assert.equal(row.find('td:eq(1)').text(), '' + firstEvaluation.priority, 'Priority');
assert.equal(row.find('td:eq(2)').text(), firstEvaluation.triggeredBy, 'Triggered By');
assert.equal(row.find('td:eq(3)').text(), firstEvaluation.status, 'Status');
});
});
test('when the job has placement failures, they are called out', function(assert) {
job = server.create('job', { failedPlacements: true });
const failedEvaluation = server.db.evaluations
.where({ jobId: job.id })
.filter(evaluation => evaluation.failedTGAllocs)
.sortBy('modifyIndex')
.reverse()[0];
const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs);
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(find('.placement-failures'), 'Placement failures section found');
const taskGroupLabels = findAll('.placement-failures h3.title').map(title =>
title.textContent.trim()
);
failedTaskGroupNames.forEach(name => {
assert.ok(
taskGroupLabels.find(label => label.includes(name)),
`${name} included in placement failures list`
);
assert.ok(
taskGroupLabels.find(label =>
label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1)
),
'The number of unplaced allocs = CoalescedFailures + 1'
);
});
});
});
test('when the job has no placement failures, the placement failures section is gone', function(
assert
) {
job = server.create('job', { noFailedPlacements: true });
visit(`/jobs/${job.id}`);
andThen(() => {
assert.notOk(find('.placement-failures'), 'Placement failures section not found');
});
});
test('when the job is not found, an error message is shown, but the URL persists', function(
assert
) {
visit('/jobs/not-a-real-job');
andThen(() => {
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the non-existent job is made'
);
assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Found',
'Error message is for 404'
);
});
});
moduleForAcceptance('Acceptance | job detail (with namespaces)', {
beforeEach() {
server.createList('namespace', 2);
server.create('node');
job = server.create('job', { namespaceId: server.db.namespaces[1].name });
server.createList('job', 3, { namespaceId: server.db.namespaces[0].name });
},
});
test('when there are namespaces, the job detail page states the namespace for the job', function(
assert
) {
const namespace = server.db.namespaces.find(job.namespaceId);
visit(`/jobs/${job.id}?namespace=${namespace.name}`);
andThen(() => {
assert.ok(
findAll('.job-stats span')[2].textContent.includes(namespace.name),
'Namespace included in stats'
);
});
});
test('when switching namespaces, the app redirects to /jobs with the new namespace', function(
assert
) {
const namespace = server.db.namespaces.find(job.namespaceId);
const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name;
const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace;
visit(`/jobs/${job.id}?namespace=${namespace.name}`);
andThen(() => {
selectChoose('.namespace-switcher', label);
});
andThen(() => {
assert.equal(currentURL().split('?')[0], '/jobs', 'Navigated to /jobs');
const jobs = server.db.jobs
.where({ namespace: otherNamespace })
.sortBy('modifyIndex')
.reverse();
assert.equal(findAll('.job-row').length, jobs.length, 'Shows the right number of jobs');
jobs.forEach((job, index) => {
assert.equal(
$(findAll('.job-row')[index])
.find('td:eq(0)')
.text()
.trim(),
job.name,
`Job ${index} is right`
);
});
});
});