open-nomad/ui/tests/integration/job-editor-test.js

493 lines
13 KiB
JavaScript

import { getOwner } from '@ember/application';
import { assign } from '@ember/polyfills';
import { run } from '@ember/runloop';
import { test, moduleForComponent } from 'ember-qunit';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
import { create } from 'ember-cli-page-object';
import sinon from 'sinon';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { getCodeMirrorInstance } from 'nomad-ui/tests/helpers/codemirror';
import jobEditor from 'nomad-ui/tests/pages/components/job-editor';
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
const Editor = create(jobEditor());
moduleForComponent('job-editor', 'Integration | Component | job-editor', {
integration: true,
beforeEach() {
window.localStorage.clear();
fragmentSerializerInitializer(getOwner(this));
// Normally getCodeMirrorInstance is a registered test helper,
// but those registered test helpers only work in acceptance tests.
window._getCodeMirrorInstance = window.getCodeMirrorInstance;
window.getCodeMirrorInstance = getCodeMirrorInstance(getOwner(this));
this.store = getOwner(this).lookup('service:store');
this.server = startMirage();
// Required for placing allocations (a result of creating jobs)
this.server.create('node');
Editor.setContext(this);
},
afterEach() {
this.server.shutdown();
Editor.removeContext();
window.getCodeMirrorInstance = window._getCodeMirrorInstance;
delete window._getCodeMirrorInstance;
},
});
const newJobName = 'new-job';
const newJobTaskGroupName = 'redis';
const jsonJob = overrides => {
return JSON.stringify(
assign(
{},
{
Name: newJobName,
Namespace: 'default',
Datacenters: ['dc1'],
Priority: 50,
TaskGroups: [
{
Name: newJobTaskGroupName,
Tasks: [
{
Name: 'redis',
Driver: 'docker',
},
],
},
],
},
overrides
),
null,
2
);
};
const hclJob = () => `
job "${newJobName}" {
namespace = "default"
datacenters = ["dc1"]
task "${newJobTaskGroupName}" {
driver = "docker"
}
}
`;
const commonTemplate = hbs`
{{job-editor
job=job
context=context
onSubmit=onSubmit}}
`;
const cancelableTemplate = hbs`
{{job-editor
job=job
context=context
cancelable=true
onSubmit=onSubmit
onCancel=onCancel}}
`;
const renderNewJob = (component, job) => () => {
component.setProperties({ job, onSubmit: sinon.spy(), context: 'new' });
component.render(commonTemplate);
return wait();
};
const renderEditJob = (component, job) => () => {
component.setProperties({ job, onSubmit: sinon.spy(), onCancel: sinon.spy(), context: 'edit' });
component.render(cancelableTemplate);
};
const planJob = spec => () => {
Editor.editor.fillIn(spec);
return wait().then(() => {
Editor.plan();
return wait();
});
};
test('the default state is an editor with an explanation popup', function(assert) {
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(() => {
assert.ok(Editor.editorHelp.isPresent, 'Editor explanation popup is present');
assert.ok(Editor.editor.isPresent, 'Editor is present');
});
});
test('the explanation popup can be dismissed', function(assert) {
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(() => {
Editor.editorHelp.dismiss();
return wait();
})
.then(() => {
assert.notOk(Editor.editorHelp.isPresent, 'Editor explanation popup is gone');
assert.equal(
window.localStorage.nomadMessageJobEditor,
'false',
'Dismissal is persisted in localStorage'
);
});
});
test('the explanation popup is not shown once the dismissal state is set in localStorage', function(assert) {
window.localStorage.nomadMessageJobEditor = 'false';
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(() => {
assert.notOk(Editor.editorHelp.isPresent, 'Editor explanation popup is gone');
});
});
test('submitting a json job skips the parse endpoint', function(assert) {
const spec = jsonJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
const requests = this.server.pretender.handledRequests.mapBy('url');
assert.notOk(requests.includes('/v1/jobs/parse'), 'JSON job spec is not parsed');
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'JSON job spec is still planned');
});
});
test('submitting an hcl job requires the parse endpoint', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
const requests = this.server.pretender.handledRequests.mapBy('url');
assert.ok(requests.includes('/v1/jobs/parse'), 'HCL job spec is parsed first');
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'HCL job spec is planned');
assert.ok(
requests.indexOf('/v1/jobs/parse') < requests.indexOf(`/v1/job/${newJobName}/plan`),
'Parse comes before Plan'
);
});
});
test('when a job is successfully parsed and planned, the plan is shown to the user', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
assert.ok(Editor.planOutput, 'The plan is outputted');
assert.notOk(Editor.editor.isPresent, 'The editor is replaced with the plan output');
assert.ok(Editor.planHelp.isPresent, 'The plan explanation popup is shown');
});
});
test('from the plan screen, the cancel button goes back to the editor with the job still in tact', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
Editor.cancel();
return wait();
})
.then(() => {
assert.ok(Editor.editor.isPresent, 'The editor is shown again');
assert.equal(
Editor.editor.contents,
spec,
'The spec that was planned is still in the editor'
);
});
});
test('when parse fails, the parse error message is shown', function(assert) {
const spec = hclJob();
const errorMessage = 'Parse Failed!! :o';
let job;
run(() => {
job = this.store.createRecord('job');
});
this.server.pretender.post('/v1/jobs/parse', () => [400, {}, errorMessage]);
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
assert.notOk(Editor.planError.isPresent, 'Plan error is not shown');
assert.notOk(Editor.runError.isPresent, 'Run error is not shown');
assert.ok(Editor.parseError.isPresent, 'Parse error is shown');
assert.equal(
Editor.parseError.message,
errorMessage,
'The error message from the server is shown in the error in the UI'
);
});
});
test('when plan fails, the plan error message is shown', function(assert) {
const spec = hclJob();
const errorMessage = 'Plan Failed!! :o';
let job;
run(() => {
job = this.store.createRecord('job');
});
this.server.pretender.post(`/v1/job/${newJobName}/plan`, () => [400, {}, errorMessage]);
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown');
assert.notOk(Editor.runError.isPresent, 'Run error is not shown');
assert.ok(Editor.planError.isPresent, 'Plan error is shown');
assert.equal(
Editor.planError.message,
errorMessage,
'The error message from the server is shown in the error in the UI'
);
});
});
test('when run fails, the run error message is shown', function(assert) {
const spec = hclJob();
const errorMessage = 'Run Failed!! :o';
let job;
run(() => {
job = this.store.createRecord('job');
});
this.server.pretender.post('/v1/jobs', () => [400, {}, errorMessage]);
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
Editor.run();
return wait();
})
.then(() => {
assert.notOk(Editor.planError.isPresent, 'Plan error is not shown');
assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown');
assert.ok(Editor.runError.isPresent, 'Run error is shown');
assert.equal(
Editor.runError.message,
errorMessage,
'The error message from the server is shown in the error in the UI'
);
});
});
test('when the scheduler dry-run has warnings, the warnings are shown to the user', function(assert) {
const spec = jsonJob({ Unschedulable: true });
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
assert.ok(
Editor.dryRunMessage.errored,
'The scheduler dry-run message is in the warning state'
);
assert.notOk(
Editor.dryRunMessage.succeeded,
'The success message is not shown in addition to the warning message'
);
assert.ok(
Editor.dryRunMessage.body.includes(newJobTaskGroupName),
'The scheduler dry-run message includes the warning from send back by the API'
);
});
});
test('when the scheduler dry-run has no warnings, a success message is shown to the user', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
assert.ok(
Editor.dryRunMessage.succeeded,
'The scheduler dry-run message is in the success state'
);
assert.notOk(
Editor.dryRunMessage.errored,
'The warning message is not shown in addition to the success message'
);
});
});
test('when a job is submitted in the edit context, a POST request is made to the update job endpoint', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderEditJob(this, job))
.then(planJob(spec))
.then(() => {
Editor.run();
})
.then(() => {
const requests = this.server.pretender.handledRequests
.filterBy('method', 'POST')
.mapBy('url');
assert.ok(requests.includes(`/v1/job/${newJobName}`), 'A request was made to job update');
assert.notOk(requests.includes('/v1/jobs'), 'A request was not made to job create');
});
});
test('when a job is submitted in the new context, a POST request is made to the create job endpoint', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
Editor.run();
})
.then(() => {
const requests = this.server.pretender.handledRequests
.filterBy('method', 'POST')
.mapBy('url');
assert.ok(requests.includes('/v1/jobs'), 'A request was made to job create');
assert.notOk(
requests.includes(`/v1/job/${newJobName}`),
'A request was not made to job update'
);
});
});
test('when a job is successfully submitted, the onSubmit hook is called', function(assert) {
const spec = hclJob();
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(planJob(spec))
.then(() => {
Editor.run();
return wait();
})
.then(() => {
assert.ok(
this.get('onSubmit').calledWith(newJobName, 'default'),
'The onSubmit hook was called with the correct arguments'
);
});
});
test('when the job-editor cancelable flag is false, there is no cancel button in the header', function(assert) {
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderNewJob(this, job))
.then(() => {
assert.notOk(Editor.cancelEditingIsAvailable, 'No way to cancel editing');
});
});
test('when the job-editor cancelable flag is true, there is a cancel button in the header', function(assert) {
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderEditJob(this, job))
.then(() => {
assert.ok(Editor.cancelEditingIsAvailable, 'Cancel editing button exists');
});
});
test('when the job-editor cancel button is clicked, the onCancel hook is called', function(assert) {
let job;
run(() => {
job = this.store.createRecord('job');
});
return wait()
.then(renderEditJob(this, job))
.then(() => {
Editor.cancelEditing();
})
.then(() => {
assert.ok(this.get('onCancel').calledOnce, 'The onCancel hook was called');
});
});