open-nomad/ui/tests/acceptance/jobs-list-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

645 lines
19 KiB
JavaScript

/* eslint-disable qunit/require-expect */
import { currentURL } from '@ember/test-helpers';
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 pageSizeSelect from './behaviors/page-size-select';
import JobsList from 'nomad-ui/tests/pages/jobs/list';
import Layout from 'nomad-ui/tests/pages/layout';
let managementToken, clientToken;
module('Acceptance | jobs list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
// Required for placing allocations (a result of creating jobs)
server.create('node');
managementToken = server.create('token');
clientToken = server.create('token');
window.localStorage.clear();
window.localStorage.nomadTokenSecret = managementToken.secretId;
});
test('it passes an accessibility audit', async function (assert) {
await JobsList.visit();
await a11yAudit(assert);
});
test('visiting /jobs', async function (assert) {
await JobsList.visit();
assert.equal(currentURL(), '/jobs');
assert.equal(document.title, 'Jobs - Nomad');
});
test('/jobs should list the first page of jobs sorted by modify index', async function (assert) {
const jobsCount = JobsList.pageSize + 1;
server.createList('job', jobsCount, { createAllocations: false });
await JobsList.visit();
const sortedJobs = server.db.jobs.sortBy('modifyIndex').reverse();
assert.equal(JobsList.jobs.length, JobsList.pageSize);
JobsList.jobs.forEach((job, index) => {
assert.equal(job.name, sortedJobs[index].name, 'Jobs are ordered');
});
});
test('each job row should contain information about the job', async function (assert) {
server.createList('job', 2);
const job = server.db.jobs.sortBy('modifyIndex').reverse()[0];
const taskGroups = server.db.taskGroups.where({ jobId: job.id });
await JobsList.visit();
const jobRow = JobsList.jobs.objectAt(0);
assert.equal(jobRow.name, job.name, 'Name');
assert.notOk(jobRow.hasNamespace);
assert.equal(jobRow.link, `/ui/jobs/${job.id}`, 'Detail Link');
assert.equal(jobRow.status, job.status, 'Status');
assert.equal(jobRow.type, typeForJob(job), 'Type');
assert.equal(jobRow.priority, job.priority, 'Priority');
assert.equal(jobRow.taskGroups, taskGroups.length, '# Groups');
});
test('each job row should link to the corresponding job', async function (assert) {
server.create('job');
const job = server.db.jobs[0];
await JobsList.visit();
await JobsList.jobs.objectAt(0).clickName();
assert.equal(currentURL(), `/jobs/${job.id}`);
});
test('the new job button transitions to the new job page', async function (assert) {
await JobsList.visit();
await JobsList.runJobButton.click();
assert.equal(currentURL(), '/jobs/run');
});
test('the job run button is disabled when the token lacks permission', async function (assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobsList.visit();
assert.ok(JobsList.runJobButton.isDisabled);
await JobsList.runJobButton.click();
assert.equal(currentURL(), '/jobs');
});
test('the anonymous policy is fetched to check whether to show the job run button', async function (assert) {
window.localStorage.removeItem('nomadTokenSecret');
server.create('policy', {
id: 'anonymous',
name: 'anonymous',
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: ['list-jobs', 'submit-job'],
},
],
},
});
await JobsList.visit();
assert.notOk(JobsList.runJobButton.isDisabled);
});
test('when there are no jobs, there is an empty message', async function (assert) {
await JobsList.visit();
assert.ok(JobsList.isEmpty, 'There is an empty message');
assert.equal(
JobsList.emptyState.headline,
'No Jobs',
'The message is appropriate'
);
});
test('when there are jobs, but no matches for a search result, there is an empty message', async function (assert) {
server.create('job', { name: 'cat 1' });
server.create('job', { name: 'cat 2' });
await JobsList.visit();
await JobsList.search.fillIn('dog');
assert.ok(JobsList.isEmpty, 'The empty message is shown');
assert.equal(
JobsList.emptyState.headline,
'No Matches',
'The message is appropriate'
);
});
test('searching resets the current page', async function (assert) {
server.createList('job', JobsList.pageSize + 1, {
createAllocations: false,
});
await JobsList.visit();
await JobsList.nextPage();
assert.equal(
currentURL(),
'/jobs?page=2',
'Page query param captures page=2'
);
await JobsList.search.fillIn('foobar');
assert.equal(currentURL(), '/jobs?search=foobar', 'No page query param');
});
test('when a cluster has namespaces, each job row includes the job namespace', async function (assert) {
server.createList('namespace', 2);
server.createList('job', 2);
const job = server.db.jobs.sortBy('modifyIndex').reverse()[0];
await JobsList.visit({ namespace: '*' });
const jobRow = JobsList.jobs.objectAt(0);
assert.equal(jobRow.namespace, job.namespaceId);
});
test('when the namespace query param is set, only matching jobs are shown', async function (assert) {
server.createList('namespace', 2);
const job1 = server.create('job', {
namespaceId: server.db.namespaces[0].id,
});
const job2 = server.create('job', {
namespaceId: server.db.namespaces[1].id,
});
await JobsList.visit();
assert.equal(JobsList.jobs.length, 2, 'All jobs by default');
const firstNamespace = server.db.namespaces[0];
await JobsList.visit({ namespace: firstNamespace.id });
assert.equal(JobsList.jobs.length, 1, 'One job in the default namespace');
assert.equal(
JobsList.jobs.objectAt(0).name,
job1.name,
'The correct job is shown'
);
const secondNamespace = server.db.namespaces[1];
await JobsList.visit({ namespace: secondNamespace.id });
assert.equal(
JobsList.jobs.length,
1,
`One job in the ${secondNamespace.name} namespace`
);
assert.equal(
JobsList.jobs.objectAt(0).name,
job2.name,
'The correct job is shown'
);
});
test('when accessing jobs is forbidden, show a message with a link to the tokens page', async function (assert) {
server.pretender.get('/v1/jobs', () => [403, {}, null]);
await JobsList.visit();
assert.equal(JobsList.error.title, 'Not Authorized');
await JobsList.error.seekHelp();
assert.equal(currentURL(), '/settings/tokens');
});
function typeForJob(job) {
return job.periodic
? 'periodic'
: job.parameterized
? 'parameterized'
: job.type;
}
test('the jobs list page has appropriate faceted search options', async function (assert) {
await JobsList.visit();
assert.ok(
JobsList.facets.namespace.isHidden,
'Namespace facet not found (no namespaces)'
);
assert.ok(JobsList.facets.type.isPresent, 'Type facet found');
assert.ok(JobsList.facets.status.isPresent, 'Status facet found');
assert.ok(JobsList.facets.datacenter.isPresent, 'Datacenter facet found');
assert.ok(JobsList.facets.prefix.isPresent, 'Prefix facet found');
});
testSingleSelectFacet('Namespace', {
facet: JobsList.facets.namespace,
paramName: 'namespace',
expectedOptions: ['All (*)', 'default', 'namespace-2'],
optionToSelect: 'namespace-2',
async beforeEach() {
server.create('namespace', { id: 'default' });
server.create('namespace', { id: 'namespace-2' });
server.createList('job', 2, { namespaceId: 'default' });
server.createList('job', 2, { namespaceId: 'namespace-2' });
await JobsList.visit();
},
filter(job, selection) {
return job.namespaceId === selection;
},
});
testFacet('Type', {
facet: JobsList.facets.type,
paramName: 'type',
expectedOptions: [
'Batch',
'Parameterized',
'Periodic',
'Service',
'System',
'System Batch',
],
async beforeEach() {
server.createList('job', 2, { createAllocations: false, type: 'batch' });
server.createList('job', 2, {
createAllocations: false,
type: 'batch',
periodic: true,
childrenCount: 0,
});
server.createList('job', 2, {
createAllocations: false,
type: 'batch',
parameterized: true,
childrenCount: 0,
});
server.createList('job', 2, {
createAllocations: false,
type: 'service',
});
await JobsList.visit();
},
filter(job, selection) {
let displayType = job.type;
if (job.parameterized) displayType = 'parameterized';
if (job.periodic) displayType = 'periodic';
return selection.includes(displayType);
},
});
testFacet('Status', {
facet: JobsList.facets.status,
paramName: 'status',
expectedOptions: ['Pending', 'Running', 'Dead'],
async beforeEach() {
server.createList('job', 2, {
status: 'pending',
createAllocations: false,
childrenCount: 0,
});
server.createList('job', 2, {
status: 'running',
createAllocations: false,
childrenCount: 0,
});
server.createList('job', 2, {
status: 'dead',
createAllocations: false,
childrenCount: 0,
});
await JobsList.visit();
},
filter: (job, selection) => selection.includes(job.status),
});
testFacet('Datacenter', {
facet: JobsList.facets.datacenter,
paramName: 'dc',
expectedOptions(jobs) {
const allDatacenters = new Set(
jobs.mapBy('datacenters').reduce((acc, val) => acc.concat(val), [])
);
return Array.from(allDatacenters).sort();
},
async beforeEach() {
server.create('job', {
datacenters: ['pdx', 'lax'],
createAllocations: false,
childrenCount: 0,
});
server.create('job', {
datacenters: ['pdx', 'ord'],
createAllocations: false,
childrenCount: 0,
});
server.create('job', {
datacenters: ['lax', 'jfk'],
createAllocations: false,
childrenCount: 0,
});
server.create('job', {
datacenters: ['jfk', 'dfw'],
createAllocations: false,
childrenCount: 0,
});
server.create('job', {
datacenters: ['pdx'],
createAllocations: false,
childrenCount: 0,
});
await JobsList.visit();
},
filter: (job, selection) =>
job.datacenters.find((dc) => selection.includes(dc)),
});
testFacet('Prefix', {
facet: JobsList.facets.prefix,
paramName: 'prefix',
expectedOptions: ['hashi (3)', 'nmd (2)', 'pre (2)'],
async beforeEach() {
[
'pre-one',
'hashi_one',
'nmd.one',
'one-alone',
'pre_two',
'hashi.two',
'hashi-three',
'nmd_two',
'noprefix',
].forEach((name) => {
server.create('job', {
name,
createAllocations: false,
childrenCount: 0,
});
});
await JobsList.visit();
},
filter: (job, selection) =>
selection.find((prefix) => job.name.startsWith(prefix)),
});
test('when the facet selections result in no matches, the empty state states why', async function (assert) {
server.createList('job', 2, {
status: 'pending',
createAllocations: false,
childrenCount: 0,
});
await JobsList.visit();
await JobsList.facets.status.toggle();
await JobsList.facets.status.options.objectAt(1).toggle();
assert.ok(JobsList.isEmpty, 'There is an empty message');
assert.equal(
JobsList.emptyState.headline,
'No Matches',
'The message is appropriate'
);
});
test('the jobs list is immediately filtered based on query params', async function (assert) {
server.create('job', { type: 'batch', createAllocations: false });
server.create('job', { type: 'service', createAllocations: false });
await JobsList.visit({ type: JSON.stringify(['batch']) });
assert.equal(
JobsList.jobs.length,
1,
'Only one job shown due to query param'
);
});
test('the active namespace is carried over to the storage pages', async function (assert) {
server.createList('namespace', 2);
const namespace = server.db.namespaces[1];
await JobsList.visit();
await JobsList.facets.namespace.toggle();
await JobsList.facets.namespace.options.objectAt(2).select();
await Layout.gutter.visitStorage();
assert.equal(currentURL(), `/csi/volumes?namespace=${namespace.id}`);
});
test('when the user has a client token that has a namespace with a policy to run a job', async function (assert) {
const READ_AND_WRITE_NAMESPACE = 'read-and-write-namespace';
const READ_ONLY_NAMESPACE = 'read-only-namespace';
server.create('namespace', { id: READ_AND_WRITE_NAMESPACE });
server.create('namespace', { id: READ_ONLY_NAMESPACE });
const policy = server.create('policy', {
id: 'something',
name: 'something',
rulesJSON: {
Namespaces: [
{
Name: READ_AND_WRITE_NAMESPACE,
Capabilities: ['submit-job'],
},
{
Name: READ_ONLY_NAMESPACE,
Capabilities: ['list-job'],
},
],
},
});
clientToken.policyIds = [policy.id];
clientToken.save();
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobsList.visit({ namespace: READ_AND_WRITE_NAMESPACE });
assert.notOk(JobsList.runJobButton.isDisabled);
await JobsList.visit({ namespace: READ_ONLY_NAMESPACE });
assert.ok(JobsList.runJobButton.isDisabled);
});
pageSizeSelect({
resourceName: 'job',
pageObject: JobsList,
pageObjectList: JobsList.jobs,
async setup() {
server.createList('job', JobsList.pageSize, {
shallow: true,
createAllocations: false,
});
await JobsList.visit();
},
});
async function facetOptions(assert, beforeEach, facet, expectedOptions) {
await beforeEach();
await facet.toggle();
let expectation;
if (typeof expectedOptions === 'function') {
expectation = expectedOptions(server.db.jobs);
} else {
expectation = expectedOptions;
}
assert.deepEqual(
facet.options.map((option) => option.label.trim()),
expectation,
'Options for facet are as expected'
);
}
function testSingleSelectFacet(
label,
{ facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
) {
test(`the ${label} facet has the correct options`, async function (assert) {
await facetOptions(assert, beforeEach, facet, expectedOptions);
});
test(`the ${label} facet filters the jobs list by ${label}`, async function (assert) {
await beforeEach();
await facet.toggle();
const option = facet.options.findOneBy('label', optionToSelect);
const selection = option.key;
await option.select();
const expectedJobs = server.db.jobs
.filter((job) => filter(job, selection))
.sortBy('modifyIndex')
.reverse();
JobsList.jobs.forEach((job, index) => {
assert.equal(
job.id,
expectedJobs[index].id,
`Job at ${index} is ${expectedJobs[index].id}`
);
});
});
test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function (assert) {
await beforeEach();
await facet.toggle();
const option = facet.options.objectAt(1);
const selection = option.key;
await option.select();
assert.ok(
currentURL().includes(`${paramName}=${selection}`),
'URL has the correct query param key and value'
);
});
}
function testFacet(
label,
{ facet, paramName, beforeEach, filter, expectedOptions }
) {
test(`the ${label} facet has the correct options`, async function (assert) {
await facetOptions(assert, beforeEach, facet, expectedOptions);
});
test(`the ${label} facet filters the jobs 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 expectedJobs = server.db.jobs
.filter((job) => filter(job, selection))
.sortBy('modifyIndex')
.reverse();
JobsList.jobs.forEach((job, index) => {
assert.equal(
job.id,
expectedJobs[index].id,
`Job at ${index} is ${expectedJobs[index].id}`
);
});
});
test(`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 expectedJobs = server.db.jobs
.filter((job) => filter(job, selection))
.sortBy('modifyIndex')
.reverse();
JobsList.jobs.forEach((job, index) => {
assert.equal(
job.id,
expectedJobs[index].id,
`Job at ${index} is ${expectedJobs[index].id}`
);
});
});
test(`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.ok(
currentURL().includes(encodeURIComponent(JSON.stringify(selection))),
'URL has the correct query param key and value'
);
});
test('the run job button works when filters are set', async function (assert) {
['pre-one', 'pre-two', 'pre-three'].forEach((name) => {
server.create('job', {
name,
createAllocations: false,
childrenCount: 0,
});
});
await JobsList.visit();
await JobsList.facets.prefix.toggle();
await JobsList.facets.prefix.options[0].toggle();
await JobsList.runJobButton.click();
assert.equal(currentURL(), '/jobs/run');
});
}
});