mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
1890 lines
60 KiB
JavaScript
1890 lines
60 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/* eslint-disable qunit/require-expect */
|
|
import {
|
|
currentURL,
|
|
settled,
|
|
click,
|
|
triggerKeyEvent,
|
|
typeIn,
|
|
} 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 percySnapshot from '@percy/ember';
|
|
import faker from 'nomad-ui/mirage/faker';
|
|
|
|
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-pool');
|
|
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) {
|
|
faker.seed(1);
|
|
const jobsCount = JobsList.pageSize + 1;
|
|
server.createList('job', jobsCount, { createAllocations: true });
|
|
|
|
await JobsList.visit();
|
|
|
|
await percySnapshot(assert);
|
|
|
|
const sortedJobs = server.db.jobs
|
|
.sortBy('id')
|
|
.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];
|
|
|
|
await JobsList.visit();
|
|
|
|
const store = this.owner.lookup('service:store');
|
|
const jobInStore = await store.peekRecord(
|
|
'job',
|
|
`["${job.id}","${job.namespace}"]`
|
|
);
|
|
|
|
const jobRow = JobsList.jobs.objectAt(0);
|
|
|
|
assert.equal(jobRow.name, job.name, 'Name');
|
|
assert.notOk(jobRow.hasNamespace);
|
|
assert.equal(jobRow.nodePool, job.nodePool, 'Node Pool');
|
|
assert.equal(jobRow.link, `/ui/jobs/${job.id}@default`, 'Detail Link');
|
|
assert.equal(
|
|
jobRow.status,
|
|
jobInStore.aggregateAllocStatus.label,
|
|
'Status'
|
|
);
|
|
assert.equal(jobRow.type, typeForJob(job), 'Type');
|
|
});
|
|
|
|
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}@default`);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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) {
|
|
faker.seed(1);
|
|
await JobsList.visit();
|
|
|
|
await percySnapshot(assert);
|
|
|
|
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 click('[data-test-pager="next"]');
|
|
|
|
assert.ok(
|
|
currentURL().includes('cursorAt'),
|
|
'Page query param contains cursorAt'
|
|
);
|
|
|
|
await JobsList.search.fillIn('foobar');
|
|
|
|
assert.equal(
|
|
currentURL(),
|
|
'/jobs?filter=Name%20contains%20%22foobar%22',
|
|
'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/statuses', () => [403, {}, null]);
|
|
|
|
await JobsList.visit();
|
|
assert.equal(JobsList.error.title, 'Not Authorized');
|
|
await percySnapshot(assert);
|
|
|
|
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.nodePool.isPresent, 'Node Pools facet found');
|
|
assert.notOk(
|
|
JobsList.facets.namespace.isPresent,
|
|
'Namespace facet not found by default'
|
|
);
|
|
});
|
|
|
|
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', 'service', 'system', 'sysbatch'],
|
|
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;
|
|
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),
|
|
});
|
|
|
|
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({ filter: 'Type == batch' });
|
|
|
|
assert.equal(
|
|
JobsList.jobs.length,
|
|
1,
|
|
'Only one job shown due to query param'
|
|
);
|
|
});
|
|
|
|
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.notOk(JobsList.runJobButton.isDisabled);
|
|
});
|
|
|
|
test('when the user has no client tokens that allow them 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_ONLY_NAMESPACE });
|
|
|
|
const policy = server.create('policy', {
|
|
id: 'something',
|
|
name: 'something',
|
|
rulesJSON: {
|
|
Namespaces: [
|
|
{
|
|
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.ok(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();
|
|
},
|
|
});
|
|
|
|
test('the run job button works when filters are set', async function (assert) {
|
|
server.create('job', {
|
|
name: 'un',
|
|
createAllocations: false,
|
|
childrenCount: 0,
|
|
type: 'batch',
|
|
});
|
|
|
|
server.create('job', {
|
|
name: 'deux',
|
|
createAllocations: false,
|
|
childrenCount: 0,
|
|
type: 'system',
|
|
});
|
|
|
|
await JobsList.visit();
|
|
|
|
await JobsList.facets.type.toggle();
|
|
await JobsList.facets.type.options[0].toggle();
|
|
|
|
await JobsList.runJobButton.click();
|
|
assert.equal(currentURL(), '/jobs/run');
|
|
});
|
|
|
|
test('Parent/child jobs are displayed correctly', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 5);
|
|
|
|
let periodicJob = server.create('job', 'periodic', {
|
|
name: 'periodic',
|
|
id: 'periodic',
|
|
childrenCount: 10,
|
|
});
|
|
|
|
// Set all children of that job to have a status of "running"
|
|
server.db.jobs.where({ parentId: periodicJob.id }).forEach((job) => {
|
|
server.db.jobs.update(job.id, { status: 'running' });
|
|
});
|
|
|
|
await JobsList.visit();
|
|
|
|
assert.dom('[data-test-job-row="periodic"]').exists();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 6 },
|
|
'Even though a periodic job has 10 children, only the parent is shown'
|
|
);
|
|
|
|
assert.dom('.allocation-status-row').exists({ count: 5 });
|
|
assert
|
|
.dom('[data-test-job-row="periodic"] .allocation-status-row')
|
|
.doesNotExist('Parent job doesnt have an allocs chart');
|
|
|
|
assert
|
|
.dom('[data-test-job-row="periodic"] [data-test-job-status]')
|
|
.hasText('10 running jobs', 'Parent job status indicates running jobs');
|
|
|
|
server.db.jobs.where({ parentId: periodicJob.id }).forEach((job) => {
|
|
server.db.jobs.update(job.id, { status: 'dead' });
|
|
});
|
|
|
|
const controller = this.owner.lookup('controller:jobs.index');
|
|
let currentParams = {
|
|
per_page: 10,
|
|
};
|
|
|
|
// We have to wait for watchJobIDs to trigger the "dueling query" with watchJobs.
|
|
// Since we can't await the watchJobs promise, we set a reasonably short timeout
|
|
// to check the state of the list after the dueling query has completed.
|
|
await controller.watchJobIDs.perform(currentParams, 0);
|
|
|
|
let parentStatusUpdated = assert.async(); // watch for this to say "My tests oughta be passing by now"
|
|
const duelingQueryUpdateTime = 200;
|
|
|
|
assert.timeout(500);
|
|
|
|
setTimeout(async () => {
|
|
assert
|
|
.dom('[data-test-job-row="periodic"] [data-test-job-status]')
|
|
.hasText(
|
|
'10 completed jobs',
|
|
'Parent job status indicates complete jobs'
|
|
);
|
|
parentStatusUpdated();
|
|
|
|
await click('[data-test-job-row="periodic"]');
|
|
assert
|
|
.dom('[data-test-child-job-row]')
|
|
.exists({ count: 10 }, 'All children are shown');
|
|
}, duelingQueryUpdateTime);
|
|
|
|
await percySnapshot(assert);
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
module('Pagination', function () {
|
|
module('Buttons are appropriately disabled', function () {
|
|
test('when there are no jobs', async function (assert) {
|
|
await JobsList.visit();
|
|
assert.dom('[data-test-pager="first"]').doesNotExist();
|
|
assert.dom('[data-test-pager="previous"]').doesNotExist();
|
|
assert.dom('[data-test-pager="next"]').doesNotExist();
|
|
assert.dom('[data-test-pager="last"]').doesNotExist();
|
|
await percySnapshot(assert);
|
|
});
|
|
test('when there are fewer jobs than your page size setting', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 5);
|
|
await JobsList.visit();
|
|
assert.dom('[data-test-pager="first"]').isDisabled();
|
|
assert.dom('[data-test-pager="previous"]').isDisabled();
|
|
assert.dom('[data-test-pager="next"]').isDisabled();
|
|
assert.dom('[data-test-pager="last"]').isDisabled();
|
|
await percySnapshot(assert);
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
test('when you have plenty of jobs', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 25);
|
|
await JobsList.visit();
|
|
assert.dom('.job-row').exists({ count: 10 });
|
|
assert.dom('[data-test-pager="first"]').isDisabled();
|
|
assert.dom('[data-test-pager="previous"]').isDisabled();
|
|
assert.dom('[data-test-pager="next"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="last"]').isNotDisabled();
|
|
// Clicking next brings me to another full page
|
|
await click('[data-test-pager="next"]');
|
|
assert.dom('.job-row').exists({ count: 10 });
|
|
assert.dom('[data-test-pager="first"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="previous"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="next"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="last"]').isNotDisabled();
|
|
// clicking next again brings me to the last page, showing jobs 20-25
|
|
await click('[data-test-pager="next"]');
|
|
assert.dom('.job-row').exists({ count: 5 });
|
|
assert.dom('[data-test-pager="first"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="previous"]').isNotDisabled();
|
|
assert.dom('[data-test-pager="next"]').isDisabled();
|
|
assert.dom('[data-test-pager="last"]').isDisabled();
|
|
await percySnapshot(assert);
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
});
|
|
module('Jobs are appropriately sorted by modify index', function () {
|
|
test('on a single long page', async function (assert) {
|
|
const jobsToCreate = 25;
|
|
localStorage.setItem('nomadPageSize', '25');
|
|
createJobs(server, jobsToCreate);
|
|
await JobsList.visit();
|
|
assert.dom('.job-row').exists({ count: 25 });
|
|
// Check the data-test-modify-index attribute on each row
|
|
let rows = document.querySelectorAll('.job-row');
|
|
let modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse(),
|
|
'Jobs are sorted by modify index'
|
|
);
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
test('across multiple pages', async function (assert) {
|
|
const jobsToCreate = 90;
|
|
const pageSize = 25;
|
|
localStorage.setItem('nomadPageSize', pageSize.toString());
|
|
createJobs(server, jobsToCreate);
|
|
await JobsList.visit();
|
|
let rows = document.querySelectorAll('.job-row');
|
|
let modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(0, pageSize),
|
|
'First page is sorted by modify index'
|
|
);
|
|
// Click next
|
|
await click('[data-test-pager="next"]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(pageSize, pageSize * 2),
|
|
'Second page is sorted by modify index'
|
|
);
|
|
|
|
// Click next again
|
|
await click('[data-test-pager="next"]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(pageSize * 2, pageSize * 3),
|
|
'Third page is sorted by modify index'
|
|
);
|
|
|
|
// Click previous
|
|
await click('[data-test-pager="previous"]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(pageSize, pageSize * 2),
|
|
'Second page is sorted by modify index'
|
|
);
|
|
|
|
// Click next twice, should be the last page, and therefore fewer than pageSize jobs
|
|
await click('[data-test-pager="next"]');
|
|
await click('[data-test-pager="next"]');
|
|
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(pageSize * 3),
|
|
'Fourth page is sorted by modify index'
|
|
);
|
|
assert.equal(
|
|
rows.length,
|
|
jobsToCreate - pageSize * 3,
|
|
'Last page has fewer jobs'
|
|
);
|
|
|
|
// Go back to the first page
|
|
await click('[data-test-pager="first"]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(0, pageSize),
|
|
'First page is sorted by modify index'
|
|
);
|
|
|
|
// Click "last" to get an even number of jobs at the end of the list
|
|
await click('[data-test-pager="last"]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(-pageSize),
|
|
'Last page is sorted by modify index'
|
|
);
|
|
assert.equal(
|
|
rows.length,
|
|
pageSize,
|
|
'Last page has the correct number of jobs'
|
|
);
|
|
|
|
// type "{{" to go to the beginning
|
|
triggerKeyEvent('.page-layout', 'keydown', '{');
|
|
await triggerKeyEvent('.page-layout', 'keydown', '{');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(0, pageSize),
|
|
'Keynav takes me back to the starting page'
|
|
);
|
|
|
|
// type "]]" to go forward a page
|
|
triggerKeyEvent('.page-layout', 'keydown', ']');
|
|
await triggerKeyEvent('.page-layout', 'keydown', ']');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(jobsToCreate)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse()
|
|
.slice(pageSize, pageSize * 2),
|
|
'Keynav takes me forward a page'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
});
|
|
module('Live updates are reflected in the list', function () {
|
|
test('When you have live updates enabled, the list updates when new jobs are created', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 10);
|
|
await JobsList.visit();
|
|
assert.dom('.job-row').exists({ count: 10 });
|
|
let rows = document.querySelectorAll('.job-row');
|
|
assert.equal(rows.length, 10, 'List is still 10 rows');
|
|
let modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(10)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse(),
|
|
'Jobs are sorted by modify index'
|
|
);
|
|
assert.dom('[data-test-pager="next"]').isDisabled();
|
|
|
|
// Create a new job
|
|
server.create('job', {
|
|
namespaceId: 'default',
|
|
resourceSpec: Array(1).fill('M: 256, C: 500'),
|
|
groupAllocCount: 1,
|
|
modifyIndex: 11,
|
|
createAllocations: false,
|
|
shallow: true,
|
|
name: 'new-job',
|
|
});
|
|
|
|
const controller = this.owner.lookup('controller:jobs.index');
|
|
|
|
let currentParams = {
|
|
per_page: 10,
|
|
};
|
|
|
|
// We have to wait for watchJobIDs to trigger the "dueling query" with watchJobs.
|
|
// Since we can't await the watchJobs promise, we set a reasonably short timeout
|
|
// to check the state of the list after the dueling query has completed.
|
|
await controller.watchJobIDs.perform(currentParams, 0);
|
|
|
|
let updatedJob = assert.async(); // watch for this to say "My tests oughta be passing by now"
|
|
const duelingQueryUpdateTime = 200;
|
|
|
|
assert.timeout(500);
|
|
|
|
setTimeout(async () => {
|
|
// Order should now be 11-2
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(10)
|
|
.fill()
|
|
.map((_, i) => i + 2)
|
|
.reverse(),
|
|
'Jobs are sorted by modify index'
|
|
);
|
|
|
|
// Simulate one of the on-page jobs getting its modify-index bumped. It should bump to the top of the list.
|
|
let existingJobToUpdate = server.db.jobs.findBy(
|
|
(job) => job.modifyIndex === 5
|
|
);
|
|
server.db.jobs.update(existingJobToUpdate.id, { modifyIndex: 12 });
|
|
await controller.watchJobIDs.perform(currentParams, 0);
|
|
let updatedOnPageJob = assert.async();
|
|
|
|
setTimeout(async () => {
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
[12, 11, 10, 9, 8, 7, 6, 4, 3, 2],
|
|
'Jobs are sorted by modify index, on-page job moves up to the top, and off-page pending'
|
|
);
|
|
updatedOnPageJob();
|
|
|
|
assert.dom('[data-test-pager="next"]').isNotDisabled();
|
|
|
|
await click('[data-test-pager="next"]');
|
|
|
|
rows = document.querySelectorAll('.job-row');
|
|
assert.equal(rows.length, 1, 'List is now 1 row');
|
|
assert.equal(
|
|
rows[0].getAttribute('data-test-modify-index'),
|
|
'1',
|
|
'Job is the first job, now pushed to the second page'
|
|
);
|
|
}, duelingQueryUpdateTime);
|
|
updatedJob();
|
|
}, duelingQueryUpdateTime);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
test('When you have live updates disabled, the list does not update, but prompts you to refresh', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
localStorage.setItem('nomadLiveUpdateJobsIndex', 'false');
|
|
createJobs(server, 10);
|
|
await JobsList.visit();
|
|
assert.dom('[data-test-updates-pending-button]').doesNotExist();
|
|
|
|
let rows = document.querySelectorAll('.job-row');
|
|
assert.equal(rows.length, 10, 'List is still 10 rows');
|
|
let modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(10)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse(),
|
|
'Jobs are sorted by modify index'
|
|
);
|
|
|
|
// Create a new job
|
|
server.create('job', {
|
|
namespaceId: 'default',
|
|
resourceSpec: Array(1).fill('M: 256, C: 500'),
|
|
groupAllocCount: 1,
|
|
modifyIndex: 11,
|
|
createAllocations: false,
|
|
shallow: true,
|
|
name: 'new-job',
|
|
});
|
|
|
|
const controller = this.owner.lookup('controller:jobs.index');
|
|
|
|
let currentParams = {
|
|
per_page: 10,
|
|
};
|
|
|
|
// We have to wait for watchJobIDs to trigger the "dueling query" with watchJobs.
|
|
// Since we can't await the watchJobs promise, we set a reasonably short timeout
|
|
// to check the state of the list after the dueling query has completed.
|
|
await controller.watchJobIDs.perform(currentParams, 0);
|
|
|
|
let updatedUnshownJob = assert.async(); // watch for this to say "My tests oughta be passing by now"
|
|
const duelingQueryUpdateTime = 200;
|
|
|
|
assert.timeout(500);
|
|
|
|
setTimeout(async () => {
|
|
// Order should still be be 10-1
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
Array(10)
|
|
.fill()
|
|
.map((_, i) => i + 1)
|
|
.reverse(),
|
|
'Jobs are sorted by modify index, off-page job not showing up yet'
|
|
);
|
|
assert
|
|
.dom('[data-test-updates-pending-button]')
|
|
.exists('The refresh button is present');
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isNotDisabled(
|
|
'Next button is enabled in spite of the new job not showing up yet'
|
|
);
|
|
|
|
// Simulate one of the on-page jobs getting its modify-index bumped. It should remain in place.
|
|
let existingJobToUpdate = server.db.jobs.findBy(
|
|
(job) => job.modifyIndex === 5
|
|
);
|
|
server.db.jobs.update(existingJobToUpdate.id, { modifyIndex: 12 });
|
|
await controller.watchJobIDs.perform(currentParams, 0);
|
|
let updatedShownJob = assert.async();
|
|
|
|
setTimeout(async () => {
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
[10, 9, 8, 7, 6, 12, 4, 3, 2, 1],
|
|
'Jobs are sorted by modify index, on-page job remains in-place, and off-page pending'
|
|
);
|
|
assert
|
|
.dom('[data-test-updates-pending-button]')
|
|
.exists('The refresh button is still present');
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isNotDisabled('Next button is still enabled');
|
|
|
|
// Click the refresh button
|
|
await click('[data-test-updates-pending-button]');
|
|
rows = document.querySelectorAll('.job-row');
|
|
modifyIndexes = Array.from(rows).map((row) =>
|
|
parseInt(row.getAttribute('data-test-modify-index'))
|
|
);
|
|
assert.deepEqual(
|
|
modifyIndexes,
|
|
[12, 11, 10, 9, 8, 7, 6, 4, 3, 2],
|
|
'Jobs are sorted by modify index, after refresh'
|
|
);
|
|
assert
|
|
.dom('[data-test-updates-pending-button]')
|
|
.doesNotExist('The refresh button is gone');
|
|
updatedShownJob();
|
|
}, duelingQueryUpdateTime);
|
|
updatedUnshownJob();
|
|
}, duelingQueryUpdateTime);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
localStorage.removeItem('nomadLiveUpdateJobsIndex');
|
|
});
|
|
});
|
|
});
|
|
|
|
module('Searching and Filtering', function () {
|
|
module('Search', function () {
|
|
test('Searching reasons about whether you intended a job name or a filter expression', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 10);
|
|
await JobsList.visit();
|
|
|
|
await JobsList.search.fillIn('something-that-surely-doesnt-exist');
|
|
// check to see that we fired off a request; check handledRequests to find one with a ?filter in it
|
|
assert.ok(
|
|
server.pretender.handledRequests.find((req) =>
|
|
decodeURIComponent(req.url).includes(
|
|
'?filter=Name contains "something-that-surely-doesnt-exist"'
|
|
)
|
|
),
|
|
'A request was made with a filter query param that assumed job name'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Namespace == ns-2');
|
|
|
|
assert.ok(
|
|
server.pretender.handledRequests.find((req) =>
|
|
decodeURIComponent(req.url).includes('?filter=Namespace == ns-2')
|
|
),
|
|
'A request was made with a filter query param for a filter expression as typed'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
test('Searching by name filters the list', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 10);
|
|
server.create('job', {
|
|
name: 'hashi-one',
|
|
id: 'hashi-one',
|
|
modifyIndex: 0,
|
|
});
|
|
server.create('job', {
|
|
name: 'hashi-two',
|
|
id: 'hashi-two',
|
|
modifyIndex: 0,
|
|
});
|
|
await JobsList.visit();
|
|
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'Initially, 10 jobs are listed without any filters.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-one"]')
|
|
.doesNotExist(
|
|
'The specific job hashi-one should not appear without filtering.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-two"]')
|
|
.doesNotExist(
|
|
'The specific job hashi-two should also not appear without filtering.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('hashi-one');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Only one job should be visible when filtering by the name "hashi-one".'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-one"]')
|
|
.exists(
|
|
'The job hashi-one appears as expected when filtered by name.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-two"]')
|
|
.doesNotExist(
|
|
'The job hashi-two should not appear when filtering by "hashi-one".'
|
|
);
|
|
|
|
await JobsList.search.fillIn('hashi');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 2 },
|
|
'Two jobs should appear when the filter "hashi" matches both job names.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-one"]')
|
|
.exists(
|
|
'Job hashi-one is correctly displayed under the "hashi" filter.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-two"]')
|
|
.exists(
|
|
'Job hashi-two is correctly displayed under the "hashi" filter.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Name == hashi');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 0 },
|
|
'No jobs should appear when an incorrect filter format "Name == hashi" is used.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'All jobs reappear when the search filter is cleared.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-one"]')
|
|
.doesNotExist(
|
|
'The job hashi-one should disappear again when the filter is cleared.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="hashi-two"]')
|
|
.doesNotExist(
|
|
'The job hashi-two should disappear again when the filter is cleared.'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
test('Searching by type filters the list', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
server.createList('job', 10, {
|
|
createAllocations: false,
|
|
type: 'service',
|
|
modifyIndex: 10,
|
|
});
|
|
|
|
server.create('job', {
|
|
id: 'batch-job',
|
|
type: 'batch',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
server.create('job', {
|
|
id: 'system-job',
|
|
type: 'system',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
server.create('job', {
|
|
id: 'sysbatch-job',
|
|
type: 'sysbatch',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
server.create('job', {
|
|
id: 'sysbatch-job-2',
|
|
type: 'sysbatch',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
|
|
await JobsList.visit();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'Initial setup should show 10 jobs of type "service".'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="service"]')
|
|
.exists(
|
|
{ count: 10 },
|
|
'All initial jobs are confirmed to be of type "service".'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Type == batch');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Filtering by "Type == batch" should show exactly one job.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="batch"]')
|
|
.exists(
|
|
{ count: 1 },
|
|
'The single job of type "batch" is displayed as expected.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Type == system');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Only one job should be displayed when filtering by "Type == system".'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="system"]')
|
|
.exists(
|
|
{ count: 1 },
|
|
'The job of type "system" appears as expected.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Type == sysbatch');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 2 },
|
|
'Two jobs should be visible under the filter "Type == sysbatch".'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="sysbatch"]')
|
|
.exists(
|
|
{ count: 2 },
|
|
'Both jobs of type "sysbatch" are correctly displayed.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Type contains sys');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 3 },
|
|
'Filter "Type contains sys" should show three jobs.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="sysbatch"]')
|
|
.exists(
|
|
{ count: 2 },
|
|
'Two jobs of type "sysbatch" match the "sys" substring.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="system"]')
|
|
.exists(
|
|
{ count: 1 },
|
|
'One job of type "system" matches the "sys" substring.'
|
|
);
|
|
|
|
await JobsList.search.fillIn('Type != service');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 4 },
|
|
'Four jobs should be visible when excluding type "service".'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-type="batch"]')
|
|
.exists({ count: 1 }, 'One batch job is visible.');
|
|
assert
|
|
.dom('[data-test-job-type="system"]')
|
|
.exists({ count: 1 }, 'One system job is visible.');
|
|
assert
|
|
.dom('[data-test-job-type="sysbatch"]')
|
|
.exists({ count: 2 }, 'Two sysbatch jobs are visible.');
|
|
|
|
// Next/Last buttons are disabled when searching for the 10 services bc there's just 10
|
|
await JobsList.search.fillIn('Type == service');
|
|
assert.dom('.job-row').exists({ count: 10 });
|
|
assert.dom('[data-test-job-type="service"]').exists({ count: 10 });
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isDisabled(
|
|
'The next page button should be disabled when all jobs fit on one page.'
|
|
);
|
|
assert
|
|
.dom('[data-test-pager="last"]')
|
|
.isDisabled(
|
|
'The last page button should also be disabled under the same conditions.'
|
|
);
|
|
|
|
// But if we disinclude sysbatch we'll have 12, so next/last should be clickable
|
|
await JobsList.search.fillIn('Type != sysbatch');
|
|
assert.dom('.job-row').exists({ count: 10 });
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isNotDisabled(
|
|
'The next page button should be enabled when not all jobs are shown on one page.'
|
|
);
|
|
assert
|
|
.dom('[data-test-pager="last"]')
|
|
.isNotDisabled('The last page button should be enabled as well.');
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
test('Searching with a bad filter expression gives hints', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
createJobs(server, 10);
|
|
await JobsList.visit();
|
|
|
|
// Try with "type" instead of "Type"
|
|
await JobsList.search.fillIn('type == foo');
|
|
assert
|
|
.dom('[data-test-empty-jobs-list]')
|
|
.includesText(
|
|
'No jobs match your current filter selection: type == foo'
|
|
);
|
|
assert.dom('[data-test-filter-correction]').exists();
|
|
await percySnapshot(assert);
|
|
|
|
await JobsList.search.fillIn('foo != bar');
|
|
assert
|
|
.dom('[data-test-empty-jobs-list]')
|
|
.includesText('Did you mistype a key?');
|
|
assert.dom('[data-test-filter-suggestion]').exists();
|
|
await percySnapshot(assert);
|
|
|
|
await JobsList.search.fillIn('Name == surelyDoesntExist');
|
|
assert
|
|
.dom('[data-test-empty-jobs-list]')
|
|
.includesText(
|
|
'No jobs match your current filter selection: Name == surelyDoesntExist'
|
|
);
|
|
assert.dom('[data-test-filter-random-suggestion]').exists();
|
|
await percySnapshot(assert);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
});
|
|
module('Filtering', function () {
|
|
test('Filtering by namespace filters the list', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
|
|
server.create('namespace', {
|
|
id: 'default',
|
|
name: 'default',
|
|
});
|
|
|
|
server.create('namespace', {
|
|
id: 'ns-2',
|
|
name: 'ns-2',
|
|
});
|
|
|
|
server.createList('job', 10, {
|
|
createAllocations: false,
|
|
namespaceId: 'default',
|
|
modifyIndex: 10,
|
|
});
|
|
|
|
server.create('job', {
|
|
id: 'ns-2-job',
|
|
namespaceId: 'ns-2',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
|
|
// By default, start on "All" namespace
|
|
await JobsList.visit();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'Initial setup should show 10 jobs in the default namespace.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="ns-2-job"]')
|
|
.doesNotExist(
|
|
'The job in the ns-2 namespace should not appear without filtering.'
|
|
);
|
|
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isNotDisabled(
|
|
'11 jobs on "All" namespace, so second page is available'
|
|
);
|
|
|
|
// Toggle ns-2 namespace
|
|
await JobsList.facets.namespace.toggle();
|
|
await JobsList.facets.namespace.options[2].toggle();
|
|
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Only one job should be visible when filtering by the ns-2 namespace.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="ns-2-job"]')
|
|
.exists(
|
|
'The job in the ns-2 namespace appears as expected when filtered.'
|
|
);
|
|
|
|
// Switch to default namespace
|
|
await JobsList.facets.namespace.toggle();
|
|
await JobsList.facets.namespace.options[1].toggle();
|
|
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'All jobs reappear when the search filter is cleared.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="ns-2-job"]')
|
|
.doesNotExist(
|
|
'The job in the ns-2 namespace should disappear when the filter is cleared.'
|
|
);
|
|
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isDisabled(
|
|
'10 jobs in "Default" namespace, so second page is not available'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
test('Namespace filter options can be filtered', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
server.create('namespace', {
|
|
id: 'default',
|
|
name: 'default',
|
|
});
|
|
server.create('namespace', {
|
|
id: 'Bonderman',
|
|
name: 'Bonderman',
|
|
});
|
|
server.create('namespace', {
|
|
id: 'Robertson',
|
|
name: 'Robertson',
|
|
});
|
|
server.create('namespace', {
|
|
id: 'Rogers',
|
|
name: 'Rogers',
|
|
});
|
|
server.create('namespace', {
|
|
id: 'Verlander',
|
|
name: 'Verlander',
|
|
});
|
|
server.create('namespace', {
|
|
id: 'Miner',
|
|
name: 'Miner',
|
|
});
|
|
server.createList('job', 3, {
|
|
createAllocations: false,
|
|
namespaceId: 'default',
|
|
modifyIndex: 10,
|
|
});
|
|
server.createList('job', 3, {
|
|
createAllocations: false,
|
|
namespaceId: 'Bonderman',
|
|
modifyIndex: 10,
|
|
});
|
|
server.createList('job', 2, {
|
|
createAllocations: false,
|
|
namespaceId: 'Verlander',
|
|
modifyIndex: 10,
|
|
});
|
|
server.createList('job', 2, {
|
|
createAllocations: false,
|
|
namespaceId: 'Rogers',
|
|
modifyIndex: 10,
|
|
});
|
|
await JobsList.visit();
|
|
|
|
await JobsList.facets.namespace.toggle();
|
|
assert.dom('[data-test-namespace-filter-searchbox]').exists();
|
|
// and it should be focused
|
|
assert.dom('[data-test-namespace-filter-searchbox]').isFocused();
|
|
// and there should be 7 things there
|
|
assert.dom('[data-test-dropdown-option]').exists({ count: 7 });
|
|
await typeIn('[data-test-namespace-filter-searchbox]', 'Bonderman');
|
|
assert.dom('[data-test-dropdown-option]').exists({ count: 1 });
|
|
document.querySelector('[data-test-namespace-filter-searchbox]').value =
|
|
''; // clear
|
|
await typeIn('[data-test-namespace-filter-searchbox]', 'n');
|
|
assert.dom('[data-test-dropdown-option]').exists({ count: 4 });
|
|
await percySnapshot(assert);
|
|
});
|
|
test('Namespace filter only shows up if the server has more than one namespace', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
|
|
server.create('namespace', {
|
|
id: 'default',
|
|
name: 'default',
|
|
});
|
|
|
|
server.createList('job', 10, {
|
|
createAllocations: false,
|
|
namespaceId: 'default',
|
|
modifyIndex: 10,
|
|
});
|
|
|
|
await JobsList.visit();
|
|
assert
|
|
.dom('[data-test-facet="Namespace"]')
|
|
.doesNotExist(
|
|
'Namespace filter should not appear with only one namespace.'
|
|
);
|
|
|
|
let system = this.owner.lookup('service:system');
|
|
system.shouldShowNamespaces = true;
|
|
|
|
await settled();
|
|
|
|
assert
|
|
.dom('[data-test-facet="Namespace"]')
|
|
.exists(
|
|
'Namespace filter should appear with more than one namespace.'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
test('Filtering by status filters the list', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
server.createList('job', 10, {
|
|
createAllocations: false,
|
|
status: 'running',
|
|
modifyIndex: 10,
|
|
});
|
|
|
|
server.create('job', {
|
|
id: 'pending-job',
|
|
status: 'pending',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
|
|
server.create('job', {
|
|
id: 'dead-job',
|
|
status: 'dead',
|
|
createAllocations: false,
|
|
modifyIndex: 8,
|
|
});
|
|
|
|
await JobsList.visit();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'Initial setup should show 10 jobs in the "running" status.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="pending-job"]')
|
|
.doesNotExist(
|
|
'The job in the "pending" status should not appear without filtering.'
|
|
);
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isNotDisabled(
|
|
'10 jobs in "running" status, so second page is available'
|
|
);
|
|
|
|
await JobsList.facets.status.toggle();
|
|
await JobsList.facets.status.options[0].toggle(); // pending
|
|
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Only one job should be visible when filtering by the "pending" status.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="pending-job"]')
|
|
.exists(
|
|
'The job in the "pending" status appears as expected when filtered.'
|
|
);
|
|
|
|
assert
|
|
.dom('[data-test-pager="next"]')
|
|
.isDisabled(
|
|
'1 job in "pending" status, so second page is not available'
|
|
);
|
|
|
|
await JobsList.facets.status.options[2].toggle(); // dead
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 2 },
|
|
'Two jobs should be visible when the "dead" filter is added'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="dead-job"]')
|
|
.exists(
|
|
{ count: 1 },
|
|
'The job in the "dead" status appears as expected when filtered.'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
test('Filtering by a dynamically-generated facet: data-test-facet="Node Pool"', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
|
|
server.create('node-pool', {
|
|
id: 'pool-1',
|
|
name: 'pool-1',
|
|
});
|
|
server.create('node-pool', {
|
|
id: 'pool-2',
|
|
name: 'pool-2',
|
|
});
|
|
|
|
server.createList('job', 10, {
|
|
createAllocations: false,
|
|
nodePool: 'pool-1',
|
|
modifyIndex: 10,
|
|
});
|
|
|
|
server.create('job', {
|
|
id: 'pool-2-job',
|
|
nodePool: 'pool-2',
|
|
createAllocations: false,
|
|
modifyIndex: 9,
|
|
});
|
|
|
|
await JobsList.visit();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 10 },
|
|
'Initial setup should show 10 jobs in the "pool-1" node pool.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="pool-2-job"]')
|
|
.doesNotExist(
|
|
'The job in the "pool-2" node pool should not appear without filtering.'
|
|
);
|
|
await JobsList.facets.nodePool.toggle();
|
|
|
|
await JobsList.facets.nodePool.options[2].toggle(); // pool-2
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 1 },
|
|
'Only one job should be visible when filtering by the "pool-2" node pool.'
|
|
);
|
|
assert
|
|
.dom('[data-test-job-row="pool-2-job"]')
|
|
.exists(
|
|
'The job in the "pool-2" node pool appears as expected when filtered.'
|
|
);
|
|
|
|
localStorage.removeItem('nomadPageSize');
|
|
});
|
|
|
|
test('Combined Filtering and Searching', async function (assert) {
|
|
localStorage.setItem('nomadPageSize', '10');
|
|
// 2 service, 1 batch, 1 system, 1 sysbatch
|
|
// 3 running, 1 dead, 1 pending
|
|
server.create('job', {
|
|
id: 'job1',
|
|
name: 'Alpha Processing',
|
|
type: 'batch',
|
|
status: 'running',
|
|
});
|
|
server.create('job', {
|
|
id: 'job2',
|
|
name: 'Beta Calculation',
|
|
type: 'service',
|
|
status: 'dead',
|
|
});
|
|
server.create('job', {
|
|
id: 'job3',
|
|
name: 'Gamma Analysis',
|
|
type: 'sysbatch',
|
|
status: 'pending',
|
|
});
|
|
server.create('job', {
|
|
id: 'job4',
|
|
name: 'Delta Research',
|
|
type: 'system',
|
|
status: 'running',
|
|
});
|
|
server.create('job', {
|
|
id: 'job5',
|
|
name: 'Epsilon Development',
|
|
type: 'service',
|
|
status: 'running',
|
|
});
|
|
|
|
// All 5 jobs show up by default
|
|
await JobsList.visit();
|
|
assert.dom('.job-row').exists({ count: 5 }, 'All 5 jobs are visible');
|
|
|
|
// Toggle type to "service", should see 2 jobs
|
|
await JobsList.facets.type.toggle();
|
|
await JobsList.facets.type.options[1].toggle();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists({ count: 2 }, 'Two service jobs are visible');
|
|
|
|
// additionally, enable "batch" type
|
|
await JobsList.facets.type.options[0].toggle();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists(
|
|
{ count: 3 },
|
|
'Three jobs are visible with service and batch types'
|
|
);
|
|
assert.dom('[data-test-job-row="job1"]').exists();
|
|
assert.dom('[data-test-job-row="job2"]').exists();
|
|
assert.dom('[data-test-job-row="job5"]').exists();
|
|
|
|
// additionally, enable "running" status to filter down to just the running ones
|
|
await JobsList.facets.status.toggle();
|
|
await JobsList.facets.status.options[1].toggle();
|
|
assert
|
|
.dom('.job-row')
|
|
.exists({ count: 2 }, 'Two running service/batch jobs are visible');
|
|
assert.dom('[data-test-job-row="job1"]').exists();
|
|
assert.dom('[data-test-job-row="job5"]').exists();
|
|
assert.dom('[data-test-job-row="job2"]').doesNotExist();
|
|
|
|
// additionally, perform a search for Name != "Alpha Processing"
|
|
await JobsList.search.fillIn('Name != "Alpha Processing"');
|
|
assert
|
|
.dom('.job-row')
|
|
.exists({ count: 1 }, 'One running service job is visible');
|
|
assert.dom('[data-test-job-row="job5"]').exists();
|
|
assert.dom('[data-test-job-row="job1"]').doesNotExist();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
*
|
|
* @param {*} server
|
|
* @param {number} jobsToCreate
|
|
*/
|
|
function createJobs(server, jobsToCreate) {
|
|
for (let i = 0; i < jobsToCreate; i++) {
|
|
server.create('job', {
|
|
namespaceId: 'default',
|
|
resourceSpec: Array(1).fill('M: 256, C: 500'),
|
|
groupAllocCount: 1,
|
|
modifyIndex: i + 1,
|
|
createAllocations: false,
|
|
shallow: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
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 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.label];
|
|
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.label);
|
|
await option2.toggle();
|
|
selection.push(option2.label);
|
|
|
|
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.label);
|
|
await option2.toggle();
|
|
selection.push(option2.label);
|
|
|
|
selection.forEach((selection) => {
|
|
let capitalizedParamName =
|
|
paramName.charAt(0).toUpperCase() + paramName.slice(1);
|
|
assert.ok(
|
|
currentURL().includes(
|
|
encodeURIComponent(`${capitalizedParamName} == ${selection}`)
|
|
),
|
|
`URL has the correct query param key and value for ${selection}`
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
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.label;
|
|
await option.toggle();
|
|
|
|
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.label;
|
|
await option.toggle();
|
|
|
|
assert.ok(
|
|
currentURL().includes(`${paramName}=${selection}`),
|
|
'URL has the correct query param key and value'
|
|
);
|
|
});
|
|
}
|