open-nomad/ui/tests/acceptance/task-group-detail-test.js

725 lines
24 KiB
JavaScript
Raw Normal View History

import { currentURL, settled } from '@ember/test-helpers';
2019-03-13 00:04:16 +00:00
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
2019-09-26 18:47:07 +00:00
import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import {
formatBytes,
formatHertz,
formatScheduledBytes,
formatScheduledHertz,
} from 'nomad-ui/utils/units';
import TaskGroup from 'nomad-ui/tests/pages/jobs/job/task-group';
import Layout from 'nomad-ui/tests/pages/layout';
import pageSizeSelect from './behaviors/page-size-select';
2017-11-30 23:08:31 +00:00
import moment from 'moment';
2017-09-19 14:47:10 +00:00
let job;
let taskGroup;
let tasks;
let allocations;
let managementToken;
2017-09-19 14:47:10 +00:00
const sum = (total, n) => total + n;
2019-03-13 00:04:16 +00:00
module('Acceptance | task group detail', function(hooks) {
setupApplicationTest(hooks);
2019-03-13 01:09:19 +00:00
setupMirage(hooks);
2019-03-13 00:04:16 +00:00
hooks.beforeEach(async function() {
server.create('agent');
2017-09-19 14:47:10 +00:00
server.create('node', 'forceIPv4');
job = server.create('job', {
groupsCount: 2,
createAllocations: false,
});
const taskGroups = server.db.taskGroups.where({ jobId: job.id });
taskGroup = taskGroups[0];
tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
server.create('node', 'forceIPv4');
allocations = server.createList('allocation', 2, {
jobId: job.id,
taskGroup: taskGroup.name,
clientStatus: 'running',
2017-09-19 14:47:10 +00:00
});
// Allocations associated to a different task group on the job to
// assert that they aren't showing up in on this page in error.
server.createList('allocation', 3, {
jobId: job.id,
taskGroup: taskGroups[1].name,
clientStatus: 'running',
2017-09-19 14:47:10 +00:00
});
// Set a static name to make the search test deterministic
server.db.allocations.forEach(alloc => {
alloc.name = 'aaaaa';
});
// Mark the first alloc as rescheduled
allocations[0].update({
nextAllocation: allocations[1].id,
});
allocations[1].update({
previousAllocation: allocations[0].id,
});
managementToken = server.create('token');
window.localStorage.clear();
2019-03-13 00:04:16 +00:00
});
2017-09-19 14:47:10 +00:00
test('it passes an accessibility audit', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
await a11yAudit(assert);
});
test('/jobs/:id/:task-group should list high-level metrics for the allocation', async function(assert) {
const totalCPU = tasks.mapBy('resources.CPU').reduce(sum, 0);
const totalMemory = tasks.mapBy('resources.MemoryMB').reduce(sum, 0);
const totalMemoryMax = tasks
.map(t => t.resources.MemoryMaxMB || t.resources.MemoryMB)
.reduce(sum, 0);
2019-03-13 00:04:16 +00:00
const totalDisk = taskGroup.ephemeralDisk.SizeMB;
2017-09-19 14:47:10 +00:00
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
2019-03-13 00:04:16 +00:00
assert.equal(TaskGroup.tasksCount, `# Tasks ${tasks.length}`, '# Tasks');
assert.equal(
TaskGroup.cpu,
`Reserved CPU ${formatScheduledHertz(totalCPU, 'MHz')}`,
2019-03-13 00:04:16 +00:00
'Aggregated CPU reservation for all tasks'
);
let totalMemoryMaxAddendum = '';
if (totalMemoryMax > totalMemory) {
totalMemoryMaxAddendum = ` (${formatScheduledBytes(totalMemoryMax, 'MiB')} Max)`;
}
2019-03-13 00:04:16 +00:00
assert.equal(
TaskGroup.mem,
`Reserved Memory ${formatScheduledBytes(totalMemory, 'MiB')}${totalMemoryMaxAddendum}`,
2019-03-13 00:04:16 +00:00
'Aggregated Memory reservation for all tasks'
);
assert.equal(
TaskGroup.disk,
`Reserved Disk ${formatScheduledBytes(totalDisk, 'MiB')}`,
2019-03-13 00:04:16 +00:00
'Aggregated Disk reservation for all tasks'
);
assert.equal(document.title, `Task group ${taskGroup.name} - Job ${job.name} - Nomad`);
2019-03-13 00:04:16 +00:00
});
2017-09-19 14:47:10 +00:00
test('/jobs/:id/:task-group should have breadcrumbs for job and jobs', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.equal(Layout.breadcrumbFor('jobs.index').text, 'Jobs', 'First breadcrumb says jobs');
2019-03-13 00:04:16 +00:00
assert.equal(
Layout.breadcrumbFor('jobs.job.index').text,
2019-03-13 00:04:16 +00:00
job.name,
'Second breadcrumb says the job name'
);
assert.equal(
Layout.breadcrumbFor('jobs.job.task-group').text,
2019-03-13 00:04:16 +00:00
taskGroup.name,
'Third breadcrumb says the job name'
);
});
test('/jobs/:id/:task-group first breadcrumb should link to jobs', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
await Layout.breadcrumbFor('jobs.index').visit();
2017-09-19 14:47:10 +00:00
assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs');
});
test('/jobs/:id/:task-group second breadcrumb should link to the job for the task group', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
await Layout.breadcrumbFor('jobs.job.index').visit();
2017-09-19 14:47:10 +00:00
assert.equal(
currentURL(),
`/jobs/${job.id}`,
'Second breadcrumb links back to the job for the task group'
);
});
test('when the user has a client token that has a namespace with a policy to run and scale a job the autoscaler options should be available', async function(assert) {
window.localStorage.clear();
const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
const READ_ONLY_NAMESPACE = 'read-only-namespace';
const clientToken = server.create('token');
server.create('namespace', { id: SCALE_AND_WRITE_NAMESPACE });
const secondNamespace = server.create('namespace', { id: READ_ONLY_NAMESPACE });
job = server.create('job', {
groupCount: 0,
createAllocations: false,
shallow: true,
noActiveDeployment: true,
namespaceId: SCALE_AND_WRITE_NAMESPACE,
});
const scalingGroup = server.create('task-group', {
job,
name: 'scaling',
count: 1,
shallow: true,
withScaling: true,
});
job.update({ taskGroupIds: [scalingGroup.id] });
const job2 = server.create('job', {
groupCount: 0,
createAllocations: false,
shallow: true,
noActiveDeployment: true,
namespaceId: READ_ONLY_NAMESPACE,
});
const scalingGroup2 = server.create('task-group', {
job: job2,
name: 'scaling',
count: 1,
shallow: true,
withScaling: true,
});
job2.update({ taskGroupIds: [scalingGroup2.id] });
const policy = server.create('policy', {
id: 'something',
name: 'something',
rulesJSON: {
Namespaces: [
{
Name: SCALE_AND_WRITE_NAMESPACE,
Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
},
{
Name: READ_ONLY_NAMESPACE,
Capabilities: ['list-jobs', 'read-job'],
},
],
},
});
clientToken.policyIds = [policy.id];
clientToken.save();
window.localStorage.nomadTokenSecret = clientToken.secretId;
await TaskGroup.visit({
id: job.id,
name: scalingGroup.name,
namespace: SCALE_AND_WRITE_NAMESPACE,
});
assert.equal(currentURL(), `/jobs/${job.id}/scaling?namespace=${SCALE_AND_WRITE_NAMESPACE}`);
assert.notOk(TaskGroup.countStepper.increment.isDisabled);
await TaskGroup.visit({
id: job2.id,
name: scalingGroup2.name,
namespace: secondNamespace.name,
});
assert.equal(currentURL(), `/jobs/${job2.id}/scaling?namespace=${READ_ONLY_NAMESPACE}`);
assert.ok(TaskGroup.countStepper.increment.isDisabled);
});
test('/jobs/:id/:task-group should list one page of allocations for the task group', async function(assert) {
2019-03-13 00:04:16 +00:00
server.createList('allocation', TaskGroup.pageSize, {
jobId: job.id,
taskGroup: taskGroup.name,
clientStatus: 'running',
});
2017-09-19 14:47:10 +00:00
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
2017-09-19 14:47:10 +00:00
assert.ok(
server.db.allocations.where({ jobId: job.id }).length > TaskGroup.pageSize,
2017-09-19 14:47:10 +00:00
'There are enough allocations to invoke pagination'
);
assert.equal(
TaskGroup.allocations.length,
TaskGroup.pageSize,
2017-09-19 14:47:10 +00:00
'All allocations for the task group'
);
});
test('each allocation should show basic information about the allocation', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
2019-03-13 00:04:16 +00:00
const allocation = allocations.sortBy('modifyIndex').reverse()[0];
const allocationRow = TaskGroup.allocations.objectAt(0);
2017-09-19 14:47:10 +00:00
assert.equal(allocationRow.shortId, allocation.id.split('-')[0], 'Allocation short id');
assert.equal(
allocationRow.createTime,
moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
'Allocation create time'
);
assert.equal(
allocationRow.modifyTime,
moment(allocation.modifyTime / 1000000).fromNow(),
2017-11-30 23:08:31 +00:00
'Allocation modify time'
);
assert.equal(allocationRow.status, allocation.clientStatus, 'Client status');
assert.equal(allocationRow.jobVersion, allocation.jobVersion, 'Job Version');
assert.equal(
allocationRow.client,
server.db.nodes.find(allocation.nodeId).id.split('-')[0],
'Node ID'
);
assert.equal(
allocationRow.volume,
Object.keys(taskGroup.volumes).length ? 'Yes' : '',
'Volumes'
);
await allocationRow.visitClient();
2017-10-28 01:23:41 +00:00
assert.equal(currentURL(), `/clients/${allocation.nodeId}`, 'Node links to node page');
});
2017-09-19 14:47:10 +00:00
test('each allocation should show stats about the allocation', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
2019-03-13 00:04:16 +00:00
const allocation = allocations.sortBy('name')[0];
const allocationRow = TaskGroup.allocations.objectAt(0);
const allocStats = server.db.clientAllocationStats.find(allocation.id);
const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0);
const memoryUsed = tasks.reduce((sum, task) => sum + task.resources.MemoryMB, 0);
2019-03-13 00:04:16 +00:00
assert.equal(
allocationRow.cpu,
Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
'CPU %'
);
const roundedTicks = Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks);
2019-03-13 00:04:16 +00:00
assert.equal(
allocationRow.cpuTooltip,
`${formatHertz(roundedTicks, 'MHz')} / ${formatHertz(cpuUsed, 'MHz')}`,
2019-03-13 00:04:16 +00:00
'Detailed CPU information is in a tooltip'
);
assert.equal(
allocationRow.mem,
allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
'Memory used'
);
assert.equal(
allocationRow.memTooltip,
`${formatBytes(allocStats.resourceUsage.MemoryStats.RSS)} / ${formatBytes(
memoryUsed,
'MiB'
)}`,
2019-03-13 00:04:16 +00:00
'Detailed memory information is in a tooltip'
);
});
test('when the allocation search has no matches, there is an empty message', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
await TaskGroup.search('zzzzzz');
assert.ok(TaskGroup.isEmpty, 'Empty state is shown');
assert.equal(
TaskGroup.emptyState.headline,
'No Matches',
'Empty state has an appropriate message'
);
});
test('when the allocation has reschedule events, the allocation row is denoted with an icon', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
2019-03-13 00:04:16 +00:00
const rescheduleRow = TaskGroup.allocationFor(allocations[0].id);
const normalRow = TaskGroup.allocationFor(allocations[1].id);
2019-03-13 00:04:16 +00:00
assert.ok(rescheduleRow.rescheduled, 'Reschedule row has a reschedule icon');
assert.notOk(normalRow.rescheduled, 'Normal row has no reschedule icon');
});
test('/jobs/:id/:task-group should present task lifecycles', async function(assert) {
job = server.create('job', {
groupsCount: 2,
groupTaskCount: 3,
});
const taskGroups = server.db.taskGroups.where({ jobId: job.id });
taskGroup = taskGroups[0];
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.ok(TaskGroup.lifecycleChart.isPresent);
assert.equal(TaskGroup.lifecycleChart.title, 'Task Lifecycle Configuration');
tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
const taskNames = tasks.mapBy('name');
// This is thoroughly tested in allocation detail tests, so this mostly checks whats different
assert.equal(TaskGroup.lifecycleChart.tasks.length, 3);
TaskGroup.lifecycleChart.tasks.forEach(Task => {
assert.ok(taskNames.includes(Task.name));
assert.notOk(Task.isActive);
assert.notOk(Task.isFinished);
});
});
test('when the task group depends on volumes, the volumes table is shown', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.ok(TaskGroup.hasVolumes);
assert.equal(TaskGroup.volumes.length, Object.keys(taskGroup.volumes).length);
});
test('when the task group does not depend on volumes, the volumes table is not shown', async function(assert) {
job = server.create('job', { noHostVolumes: true, shallow: true });
taskGroup = server.db.taskGroups.where({ jobId: job.id })[0];
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.notOk(TaskGroup.hasVolumes);
});
test('each row in the volumes table lists information about the volume', async function(assert) {
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
TaskGroup.volumes[0].as(volumeRow => {
const volume = taskGroup.volumes[volumeRow.name];
assert.equal(volumeRow.name, volume.Name);
assert.equal(volumeRow.type, volume.Type);
assert.equal(volumeRow.source, volume.Source);
assert.equal(volumeRow.permissions, volume.ReadOnly ? 'Read' : 'Read/Write');
});
});
test('the count stepper sends the appropriate POST request', async function(assert) {
window.localStorage.nomadTokenSecret = managementToken.secretId;
job = server.create('job', {
groupCount: 0,
createAllocations: false,
shallow: true,
noActiveDeployment: true,
});
const scalingGroup = server.create('task-group', {
job,
name: 'scaling',
count: 1,
shallow: true,
withScaling: true,
});
job.update({ taskGroupIds: [scalingGroup.id] });
await TaskGroup.visit({ id: job.id, name: scalingGroup.name });
await TaskGroup.countStepper.increment.click();
await settled();
const scaleRequest = server.pretender.handledRequests.find(
req => req.method === 'POST' && req.url.endsWith('/scale')
);
const requestBody = JSON.parse(scaleRequest.requestBody);
assert.equal(requestBody.Target.Group, scalingGroup.name);
assert.equal(requestBody.Count, scalingGroup.count + 1);
});
test('the count stepper is disabled when a deployment is running', async function(assert) {
window.localStorage.nomadTokenSecret = managementToken.secretId;
job = server.create('job', {
groupCount: 0,
createAllocations: false,
shallow: true,
activeDeployment: true,
});
const scalingGroup = server.create('task-group', {
job,
name: 'scaling',
count: 1,
shallow: true,
withScaling: true,
});
job.update({ taskGroupIds: [scalingGroup.id] });
await TaskGroup.visit({ id: job.id, name: scalingGroup.name });
assert.ok(TaskGroup.countStepper.input.isDisabled);
assert.ok(TaskGroup.countStepper.increment.isDisabled);
assert.ok(TaskGroup.countStepper.decrement.isDisabled);
});
test('when the job for the task group is not found, an error message is shown, but the URL persists', async function(assert) {
await TaskGroup.visit({ id: 'not-a-real-job', name: 'not-a-real-task-group' });
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/not-a-real-task-group', 'The URL persists');
assert.ok(TaskGroup.error.isPresent, 'Error message is shown');
assert.equal(TaskGroup.error.title, 'Not Found', 'Error message is for 404');
});
test('when the task group is not found on the job, an error message is shown, but the URL persists', async function(assert) {
await TaskGroup.visit({ id: job.id, name: 'not-a-real-task-group' });
assert.ok(
server.pretender.handledRequests
.filterBy('status', 200)
.mapBy('url')
.includes(`/v1/job/${job.id}`),
'A request to the job is made and succeeds'
);
assert.equal(currentURL(), `/jobs/${job.id}/not-a-real-task-group`, 'The URL persists');
assert.ok(TaskGroup.error.isPresent, 'Error message is shown');
assert.equal(TaskGroup.error.title, 'Not Found', 'Error message is for 404');
});
pageSizeSelect({
resourceName: 'allocation',
pageObject: TaskGroup,
pageObjectList: TaskGroup.allocations,
async setup() {
server.createList('allocation', TaskGroup.pageSize, {
jobId: job.id,
taskGroup: taskGroup.name,
clientStatus: 'running',
});
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
},
});
2020-07-28 06:18:10 +00:00
test('when a task group has no scaling events, there is no recent scaling events section', async function(assert) {
const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name);
taskGroupScale.update({ events: [] });
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.notOk(TaskGroup.hasScaleEvents);
});
test('the recent scaling events section shows all recent scaling events in reverse chronological order', async function(assert) {
const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name);
taskGroupScale.update({
events: [
server.create('scale-event', { error: true }),
server.create('scale-event', { error: true }),
server.create('scale-event', { error: true }),
server.create('scale-event', { error: true }),
server.create('scale-event', { count: 3, error: false }),
server.create('scale-event', { count: 1, error: false }),
],
});
2020-07-28 06:18:10 +00:00
const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse();
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.ok(TaskGroup.hasScaleEvents);
assert.notOk(TaskGroup.hasScalingTimeline);
2020-07-28 06:18:10 +00:00
scaleEvents.forEach((scaleEvent, idx) => {
const ScaleEvent = TaskGroup.scaleEvents[idx];
assert.equal(ScaleEvent.time, moment(scaleEvent.time / 1000000).format('MMM DD HH:mm:ss ZZ'));
assert.equal(ScaleEvent.message, scaleEvent.message);
if (scaleEvent.count != null) {
assert.equal(ScaleEvent.count, scaleEvent.count);
}
2020-07-28 06:18:10 +00:00
if (scaleEvent.error) {
assert.ok(ScaleEvent.error);
}
if (Object.keys(scaleEvent.meta).length) {
assert.ok(ScaleEvent.isToggleable);
} else {
assert.notOk(ScaleEvent.isToggleable);
}
});
});
test('when a task group has at least two count scaling events and the count scaling events outnumber the non-count scaling events, a timeline is shown in addition to the accordion', async function(assert) {
const taskGroupScale = job.jobScale.taskGroupScales.models.find(m => m.name === taskGroup.name);
taskGroupScale.update({
events: [
server.create('scale-event', { error: true }),
server.create('scale-event', { error: true }),
server.create('scale-event', { count: 7, error: false }),
server.create('scale-event', { count: 10, error: false }),
server.create('scale-event', { count: 2, error: false }),
server.create('scale-event', { count: 3, error: false }),
server.create('scale-event', { count: 2, error: false }),
server.create('scale-event', { count: 9, error: false }),
server.create('scale-event', { count: 1, error: false }),
],
});
const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse();
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
assert.ok(TaskGroup.hasScaleEvents);
assert.ok(TaskGroup.hasScalingTimeline);
assert.equal(
TaskGroup.scalingAnnotations.length,
scaleEvents.filter(ev => ev.count == null).length
);
});
testFacet('Status', {
facet: TaskGroup.facets.status,
paramName: 'status',
expectedOptions: ['Pending', 'Running', 'Complete', 'Failed', 'Lost'],
async beforeEach() {
['pending', 'running', 'complete', 'failed', 'lost'].forEach(s => {
server.createList('allocation', 5, { clientStatus: s });
});
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
},
filter: (alloc, selection) =>
alloc.jobId == job.id &&
alloc.taskGroup == taskGroup.name &&
selection.includes(alloc.clientStatus),
});
testFacet('Client', {
facet: TaskGroup.facets.client,
paramName: 'client',
expectedOptions(allocs) {
return Array.from(
new Set(
allocs
.filter(alloc => alloc.jobId == job.id && alloc.taskGroup == taskGroup.name)
.mapBy('nodeId')
.map(id => id.split('-')[0])
)
).sort();
},
async beforeEach() {
const nodes = server.createList('node', 3, 'forceIPv4');
nodes.forEach(node =>
server.createList('allocation', 5, {
nodeId: node.id,
jobId: job.id,
taskGroup: taskGroup.name,
})
);
await TaskGroup.visit({ id: job.id, name: taskGroup.name });
},
filter: (alloc, selection) =>
alloc.jobId == job.id &&
alloc.taskGroup == taskGroup.name &&
selection.includes(alloc.nodeId.split('-')[0]),
});
});
function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
test(`facet ${label} | the ${label} facet has the correct options`, async function(assert) {
await beforeEach();
await facet.toggle();
let expectation;
if (typeof expectedOptions === 'function') {
expectation = expectedOptions(server.db.allocations);
} else {
expectation = expectedOptions;
}
assert.deepEqual(
facet.options.map(option => option.label.trim()),
expectation,
'Options for facet are as expected'
);
});
test(`facet ${label} | the ${label} facet filters the allocations list by ${label}`, async function(assert) {
let option;
await beforeEach();
await facet.toggle();
option = facet.options.objectAt(0);
await option.toggle();
const selection = [option.key];
const expectedAllocs = server.db.allocations
.filter(alloc => filter(alloc, selection))
.sortBy('modifyIndex')
.reverse();
TaskGroup.allocations.forEach((alloc, index) => {
assert.equal(
alloc.id,
expectedAllocs[index].id,
`Allocation at ${index} is ${expectedAllocs[index].id}`
);
});
});
test(`facet ${label} | selecting multiple options in the ${label} facet results in a broader search`, async function(assert) {
const selection = [];
await beforeEach();
await facet.toggle();
const option1 = facet.options.objectAt(0);
const option2 = facet.options.objectAt(1);
await option1.toggle();
selection.push(option1.key);
await option2.toggle();
selection.push(option2.key);
const expectedAllocs = server.db.allocations
.filter(alloc => filter(alloc, selection))
.sortBy('modifyIndex')
.reverse();
TaskGroup.allocations.forEach((alloc, index) => {
assert.equal(
alloc.id,
expectedAllocs[index].id,
`Allocation at ${index} is ${expectedAllocs[index].id}`
);
});
});
test(`facet ${label} | selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) {
const selection = [];
await beforeEach();
await facet.toggle();
const option1 = facet.options.objectAt(0);
const option2 = facet.options.objectAt(1);
await option1.toggle();
selection.push(option1.key);
await option2.toggle();
selection.push(option2.key);
assert.equal(
currentURL(),
`/jobs/${job.id}/${taskGroup.name}?${paramName}=${encodeURIComponent(
JSON.stringify(selection)
)}`,
'URL has the correct query param key and value'
);
});
}