e096a0a5ab
* chore: upgrade forward compatible packages * chore: v3.20.2...v3.24.0 * chore: silence string prototype extension deprecation * refact: don't test clicking disabled button job-list Recent test-helper upgrades will guard against clicking disabled buttons as this is not something that real users can do. We need to change our tests accordingly. * fix: await async test helper `expectError` We have to await this async test function otherwise the test's rendering context will be torn down before we run assertions against it. * fix: don't try to click disabled two-step-button Recent test-helper updates prohibit clicking disabled buttons. We need to adapt the tests accordingly. * fix: recommendation-accordion Use up-to-date semantics for handling list-accordion closing in recommendation-accordion. * fixes toggling recommendation-accordion toggle. * fix: simple-unless linting error application.hbs There's no reason to use unless here - we can use if instead. * fix: no-quoteless-attributes recommendation accordion * fix: no-quoteless-attributes recommendation-chart * fix: allow `unless` - global-header.hbs This is a valid use of unless in our opinion. * fix: allow unless in job-diff This is not a great use for unless but we don't want to change this behavior atm. * fix: no-attrs-in-components list-pager There is no need to use this.attrs in classic components. When we will convert to glimmer we will use `@`-instead. * fix: simple-unless job/definition We can convert to a simple if here. * fix: allow inline-styles stats-box component To make linter happy. * fix: disable no-action and no-invalid-interactive Will be adressed in follow-up PRs. * chore: update ember-classic-decorator to latest * chore: upgrade ember-can to latest * chore: upgrade ember-composable-helpers to latest * chore: upgrade ember-concurrency * fix: recomputation deprecation `Trigger` schedule `do` on actions queue to work around recomputation deprecation when triggering Trigger on `did-insert`. * chore: upgrade ember-cli-string-helpers * chore: upgrade ember-copy * chore: upgrade ember-data-model-fragments * chore: upgrade ember-deprecation-workflow * chore: upgrade ember-inline-svg * chore: upgrade ember-modifier * chore: upgrade ember-truth-helpers * chore: upgrade ember-moment & ember-cli-moment-shim * chore: upgrade ember-power-select * chore: upgrade ember-responsive * chore: upgrade ember-sinon * chore: upgrade ember-cli-mirage For now we will stay on 2.2 - upgrades > 2.3 break the build. * chore: upgrade 3.24.0 to 3.28.5 * fix: add missing classic decorators on adapters * fix: missing classic decorators to serializers * fix: don't reopen Ember.Object anymore * fix: remove unused useNativeEvents ember-cli-page-objects doesn't provide this method anymore * fix: add missing attributeBindings for test-selectors ember-test-selectors doesn't provides automatic bindings for data-test-* attributes anymore. * fix: classic decorator for application serializer test * fix: remove `removeContext` from tests. It is unneeded and ember-cli-page-objects doesn't provides this method anymore. * fix: remove deprecations `run.*`-invocations * fix: `collapseWhitespace` in optimize test * fix: make sure to load async relationship before access * fix: dependent keys for relationship computeds We need to add `*.isFulfilled` as dependent keys for computeds that access async relationships. * fix: `computed.read`-invocations use `read` instead * chore: prettify templates * fix: use map instead of mapBy ember-cli-page-object Doesn't work with updated ember-cli-page-object anymore. * fix: remove remaining deprecated `run.*`-calls * chore: add more deprecations deprecation-workflow * fix: `implicit-injection`-deprecation All routes that add watchers will need to inject the store-service as the store service is internally used in watchers. * fix: more implicit injection deprecations * chore: silence implicit-injection deprecation We can tackle the deprecation when we find the time. * fix: new linting errors after upgrade * fix: remove merge conflicts prettierignore * chore: upgrade to run node 12.22 when building binaries
880 lines
26 KiB
JavaScript
880 lines
26 KiB
JavaScript
/* eslint-disable qunit/require-expect */
|
||
/* eslint-disable qunit/no-conditional-assertions */
|
||
import { module, test } from 'qunit';
|
||
import { setupApplicationTest } from 'ember-qunit';
|
||
import { currentURL, visit } from '@ember/test-helpers';
|
||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||
import Response from 'ember-cli-mirage/response';
|
||
import moment from 'moment';
|
||
import { formatBytes, formatHertz, replaceMinus } from 'nomad-ui/utils/units';
|
||
|
||
import Optimize from 'nomad-ui/tests/pages/optimize';
|
||
import Layout from 'nomad-ui/tests/pages/layout';
|
||
import JobsList from 'nomad-ui/tests/pages/jobs/list';
|
||
import collapseWhitespace from '../helpers/collapse-whitespace';
|
||
|
||
let managementToken, clientToken;
|
||
|
||
function getLatestRecommendationSubmitTimeForJob(job) {
|
||
const tasks = job.taskGroups.models
|
||
.mapBy('tasks.models')
|
||
.reduce((tasks, taskModels) => tasks.concat(taskModels), []);
|
||
const recommendations = tasks.reduce(
|
||
(recommendations, task) =>
|
||
recommendations.concat(task.recommendations.models),
|
||
[]
|
||
);
|
||
return Math.max(...recommendations.mapBy('submitTime'));
|
||
}
|
||
|
||
module('Acceptance | optimize', function (hooks) {
|
||
setupApplicationTest(hooks);
|
||
setupMirage(hooks);
|
||
|
||
hooks.beforeEach(async function () {
|
||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||
|
||
server.create('node');
|
||
|
||
server.createList('namespace', 2);
|
||
|
||
const jobs = server.createList('job', 2, {
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
namespaceId: server.db.namespaces[1].id,
|
||
});
|
||
|
||
jobs.sort((jobA, jobB) => {
|
||
return (
|
||
getLatestRecommendationSubmitTimeForJob(jobB) -
|
||
getLatestRecommendationSubmitTimeForJob(jobA)
|
||
);
|
||
});
|
||
|
||
[this.job1, this.job2] = jobs;
|
||
|
||
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 Optimize.visit();
|
||
await a11yAudit(assert);
|
||
});
|
||
|
||
test('lets recommendations be toggled, reports the choices to the recommendations API, and displays task group recommendations serially', async function (assert) {
|
||
const currentTaskGroup = this.job1.taskGroups.models[0];
|
||
const nextTaskGroup = this.job2.taskGroups.models[0];
|
||
|
||
const currentTaskGroupHasCPURecommendation = currentTaskGroup.tasks.models
|
||
.mapBy('recommendations.models')
|
||
.flat()
|
||
.find((r) => r.resource === 'CPU');
|
||
|
||
// If no CPU recommendation, will not be able to accept recommendation with all memory recommendations turned off
|
||
|
||
if (!currentTaskGroupHasCPURecommendation) {
|
||
const currentTaskGroupTask = currentTaskGroup.tasks.models[0];
|
||
this.server.create('recommendation', {
|
||
task: currentTaskGroupTask,
|
||
resource: 'CPU',
|
||
});
|
||
}
|
||
|
||
await Optimize.visit();
|
||
|
||
assert.equal(Layout.breadcrumbFor('optimize').text, 'Recommendations');
|
||
|
||
assert.equal(
|
||
Optimize.recommendationSummaries[0].slug,
|
||
`${this.job1.name} / ${currentTaskGroup.name}`
|
||
);
|
||
|
||
assert.equal(
|
||
Layout.breadcrumbFor('optimize.summary').text,
|
||
`${this.job1.name} / ${currentTaskGroup.name}`
|
||
);
|
||
|
||
assert.equal(
|
||
Optimize.recommendationSummaries[0].namespace,
|
||
this.job1.namespace
|
||
);
|
||
|
||
assert.equal(
|
||
Optimize.recommendationSummaries[1].slug,
|
||
`${this.job2.name} / ${nextTaskGroup.name}`
|
||
);
|
||
|
||
const currentRecommendations = currentTaskGroup.tasks.models.reduce(
|
||
(recommendations, task) =>
|
||
recommendations.concat(task.recommendations.models),
|
||
[]
|
||
);
|
||
const latestSubmitTime = Math.max(
|
||
...currentRecommendations.mapBy('submitTime')
|
||
);
|
||
|
||
Optimize.recommendationSummaries[0].as((summary) => {
|
||
assert.equal(
|
||
summary.date,
|
||
moment(new Date(latestSubmitTime / 1000000)).format(
|
||
'MMM DD HH:mm:ss ZZ'
|
||
)
|
||
);
|
||
|
||
const currentTaskGroupAllocations = server.schema.allocations.where({
|
||
jobId: currentTaskGroup.job.name,
|
||
taskGroup: currentTaskGroup.name,
|
||
});
|
||
assert.equal(summary.allocationCount, currentTaskGroupAllocations.length);
|
||
|
||
const { currCpu, currMem } = currentTaskGroup.tasks.models.reduce(
|
||
(currentResources, task) => {
|
||
currentResources.currCpu += task.resources.CPU;
|
||
currentResources.currMem += task.resources.MemoryMB;
|
||
return currentResources;
|
||
},
|
||
{ currCpu: 0, currMem: 0 }
|
||
);
|
||
|
||
const { recCpu, recMem } = currentRecommendations.reduce(
|
||
(recommendedResources, recommendation) => {
|
||
if (recommendation.resource === 'CPU') {
|
||
recommendedResources.recCpu += recommendation.value;
|
||
} else {
|
||
recommendedResources.recMem += recommendation.value;
|
||
}
|
||
|
||
return recommendedResources;
|
||
},
|
||
{ recCpu: 0, recMem: 0 }
|
||
);
|
||
|
||
const cpuDiff = recCpu > 0 ? recCpu - currCpu : 0;
|
||
const memDiff = recMem > 0 ? recMem - currMem : 0;
|
||
|
||
const cpuSign = cpuDiff > 0 ? '+' : '';
|
||
const memSign = memDiff > 0 ? '+' : '';
|
||
|
||
const cpuDiffPercent = Math.round((100 * cpuDiff) / currCpu);
|
||
const memDiffPercent = Math.round((100 * memDiff) / currMem);
|
||
|
||
assert.equal(
|
||
replaceMinus(summary.cpu),
|
||
cpuDiff
|
||
? `${cpuSign}${formatHertz(
|
||
cpuDiff,
|
||
'MHz'
|
||
)} ${cpuSign}${cpuDiffPercent}%`
|
||
: ''
|
||
);
|
||
assert.equal(
|
||
replaceMinus(summary.memory),
|
||
memDiff
|
||
? `${memSign}${formattedMemDiff(
|
||
memDiff
|
||
)} ${memSign}${memDiffPercent}%`
|
||
: ''
|
||
);
|
||
|
||
assert.equal(
|
||
replaceMinus(summary.aggregateCpu),
|
||
cpuDiff
|
||
? `${cpuSign}${formatHertz(
|
||
cpuDiff * currentTaskGroupAllocations.length,
|
||
'MHz'
|
||
)}`
|
||
: ''
|
||
);
|
||
|
||
assert.equal(
|
||
replaceMinus(summary.aggregateMemory),
|
||
memDiff
|
||
? `${memSign}${formattedMemDiff(
|
||
memDiff * currentTaskGroupAllocations.length
|
||
)}`
|
||
: ''
|
||
);
|
||
});
|
||
|
||
assert.ok(Optimize.recommendationSummaries[0].isActive);
|
||
assert.notOk(Optimize.recommendationSummaries[1].isActive);
|
||
|
||
assert.equal(Optimize.card.slug.jobName, this.job1.name);
|
||
assert.equal(Optimize.card.slug.groupName, currentTaskGroup.name);
|
||
|
||
const summaryMemoryBefore = Optimize.recommendationSummaries[0].memory;
|
||
|
||
let toggledAnything = true;
|
||
|
||
// Toggle off all memory
|
||
if (Optimize.card.togglesTable.toggleAllMemory.isPresent) {
|
||
await Optimize.card.togglesTable.toggleAllMemory.toggle();
|
||
|
||
assert.notOk(Optimize.card.togglesTable.tasks[0].memory.isActive);
|
||
assert.notOk(Optimize.card.togglesTable.tasks[1].memory.isActive);
|
||
} else if (!Optimize.card.togglesTable.tasks[0].cpu.isDisabled) {
|
||
await Optimize.card.togglesTable.tasks[0].memory.toggle();
|
||
} else {
|
||
toggledAnything = false;
|
||
}
|
||
|
||
assert.equal(
|
||
Optimize.recommendationSummaries[0].memory,
|
||
summaryMemoryBefore,
|
||
'toggling recommendations doesn’t affect the summary table diffs'
|
||
);
|
||
|
||
const currentTaskIds = currentTaskGroup.tasks.models.mapBy('id');
|
||
const taskIdFilter = (task) => currentTaskIds.includes(task.taskId);
|
||
|
||
const cpuRecommendationIds = server.schema.recommendations
|
||
.where({ resource: 'CPU' })
|
||
.models.filter(taskIdFilter)
|
||
.mapBy('id');
|
||
|
||
const memoryRecommendationIds = server.schema.recommendations
|
||
.where({ resource: 'MemoryMB' })
|
||
.models.filter(taskIdFilter)
|
||
.mapBy('id');
|
||
|
||
const appliedIds = toggledAnything
|
||
? cpuRecommendationIds
|
||
: memoryRecommendationIds;
|
||
const dismissedIds = toggledAnything ? memoryRecommendationIds : [];
|
||
|
||
await Optimize.card.acceptButton.click();
|
||
|
||
const request = server.pretender.handledRequests
|
||
.filterBy('method', 'POST')
|
||
.pop();
|
||
const { Apply, Dismiss } = JSON.parse(request.requestBody);
|
||
|
||
assert.equal(request.url, '/v1/recommendations/apply');
|
||
|
||
assert.deepEqual(Apply, appliedIds);
|
||
assert.deepEqual(Dismiss, dismissedIds);
|
||
|
||
assert.equal(Optimize.card.slug.jobName, this.job2.name);
|
||
assert.equal(Optimize.card.slug.groupName, nextTaskGroup.name);
|
||
|
||
assert.ok(Optimize.recommendationSummaries[1].isActive);
|
||
});
|
||
|
||
test('can navigate between summaries via the table', async function (assert) {
|
||
server.createList('job', 10, {
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
namespaceId: server.db.namespaces[1].id,
|
||
});
|
||
|
||
await Optimize.visit();
|
||
await Optimize.recommendationSummaries[1].click();
|
||
|
||
assert.equal(
|
||
`${Optimize.card.slug.jobName} / ${Optimize.card.slug.groupName}`,
|
||
Optimize.recommendationSummaries[1].slug
|
||
);
|
||
assert.ok(Optimize.recommendationSummaries[1].isActive);
|
||
});
|
||
|
||
test('can visit a summary directly via URL', async function (assert) {
|
||
server.createList('job', 10, {
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
namespaceId: server.db.namespaces[1].id,
|
||
});
|
||
|
||
await Optimize.visit();
|
||
|
||
const lastSummary =
|
||
Optimize.recommendationSummaries[
|
||
Optimize.recommendationSummaries.length - 1
|
||
];
|
||
const collapsedSlug = lastSummary.slug.replace(' / ', '/');
|
||
|
||
// preferable to use page object’s visitable but it encodes the slash
|
||
await visit(
|
||
`/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`
|
||
);
|
||
|
||
assert.equal(
|
||
`${Optimize.card.slug.jobName} / ${Optimize.card.slug.groupName}`,
|
||
lastSummary.slug
|
||
);
|
||
assert.ok(lastSummary.isActive);
|
||
assert.equal(
|
||
currentURL(),
|
||
`/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`
|
||
);
|
||
});
|
||
|
||
test('when a summary is not found, an error message is shown, but the URL persists', async function (assert) {
|
||
await visit('/optimize/nonexistent/summary?namespace=anamespace');
|
||
|
||
assert.equal(
|
||
currentURL(),
|
||
'/optimize/nonexistent/summary?namespace=anamespace'
|
||
);
|
||
assert.ok(Optimize.applicationError.isPresent);
|
||
assert.equal(Optimize.applicationError.title, 'Not Found');
|
||
});
|
||
|
||
test('cannot return to already-processed summaries', async function (assert) {
|
||
await Optimize.visit();
|
||
await Optimize.card.acceptButton.click();
|
||
|
||
assert.ok(Optimize.recommendationSummaries[0].isDisabled);
|
||
|
||
await Optimize.recommendationSummaries[0].click();
|
||
|
||
assert.ok(Optimize.recommendationSummaries[1].isActive);
|
||
});
|
||
|
||
test('can dismiss a set of recommendations', async function (assert) {
|
||
await Optimize.visit();
|
||
|
||
const currentTaskGroup = this.job1.taskGroups.models[0];
|
||
const currentTaskIds = currentTaskGroup.tasks.models.mapBy('id');
|
||
const taskIdFilter = (task) => currentTaskIds.includes(task.taskId);
|
||
|
||
const idsBeforeDismissal = server.schema.recommendations
|
||
.all()
|
||
.models.filter(taskIdFilter)
|
||
.mapBy('id');
|
||
|
||
await Optimize.card.dismissButton.click();
|
||
|
||
const request = server.pretender.handledRequests
|
||
.filterBy('method', 'POST')
|
||
.pop();
|
||
const { Apply, Dismiss } = JSON.parse(request.requestBody);
|
||
|
||
assert.equal(request.url, '/v1/recommendations/apply');
|
||
|
||
assert.deepEqual(Apply, []);
|
||
assert.deepEqual(Dismiss, idsBeforeDismissal);
|
||
});
|
||
|
||
test('it displays an error encountered trying to save and proceeds to the next summary when the error is dismissed', async function (assert) {
|
||
server.post('/recommendations/apply', function () {
|
||
return new Response(500, {}, null);
|
||
});
|
||
|
||
await Optimize.visit();
|
||
await Optimize.card.acceptButton.click();
|
||
|
||
assert.ok(Optimize.error.isPresent);
|
||
assert.equal(Optimize.error.headline, 'Recommendation error');
|
||
assert.equal(
|
||
Optimize.error.errors,
|
||
'Error: Ember Data Request POST /v1/recommendations/apply returned a 500 Payload (application/json)'
|
||
);
|
||
|
||
await Optimize.error.dismiss();
|
||
assert.equal(Optimize.card.slug.jobName, this.job2.name);
|
||
});
|
||
|
||
test('it displays an empty message when there are no recommendations', async function (assert) {
|
||
server.db.recommendations.remove();
|
||
await Optimize.visit();
|
||
|
||
assert.ok(Optimize.empty.isPresent);
|
||
assert.equal(Optimize.empty.headline, 'No Recommendations');
|
||
});
|
||
|
||
test('it displays an empty message after all recommendations have been processed', async function (assert) {
|
||
await Optimize.visit();
|
||
|
||
await Optimize.card.acceptButton.click();
|
||
await Optimize.card.acceptButton.click();
|
||
|
||
assert.ok(Optimize.empty.isPresent);
|
||
});
|
||
|
||
test('it redirects to jobs and hides the gutter link when the token lacks permissions', async function (assert) {
|
||
window.localStorage.nomadTokenSecret = clientToken.secretId;
|
||
await Optimize.visit();
|
||
|
||
assert.equal(currentURL(), '/jobs?namespace=*');
|
||
assert.ok(Layout.gutter.optimize.isHidden);
|
||
});
|
||
|
||
test('it reloads partially-loaded jobs', async function (assert) {
|
||
await JobsList.visit();
|
||
await Optimize.visit();
|
||
|
||
assert.equal(Optimize.recommendationSummaries.length, 2);
|
||
});
|
||
});
|
||
|
||
module('Acceptance | optimize search and facets', function (hooks) {
|
||
setupApplicationTest(hooks);
|
||
setupMirage(hooks);
|
||
|
||
hooks.beforeEach(async function () {
|
||
server.create('feature', { name: 'Dynamic Application Sizing' });
|
||
|
||
server.create('node');
|
||
|
||
server.createList('namespace', 2);
|
||
|
||
managementToken = server.create('token');
|
||
|
||
window.localStorage.clear();
|
||
window.localStorage.nomadTokenSecret = managementToken.secretId;
|
||
});
|
||
|
||
test('search field narrows summary table results, changes the active summary if it no longer matches, and displays a no matches message when there are none', async function (assert) {
|
||
server.create('job', {
|
||
name: 'zzzzzz',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 6,
|
||
});
|
||
|
||
// Ensure this job’s recommendations are sorted to the top of the table
|
||
const futureSubmitTime = (Date.now() + 10000) * 1000000;
|
||
server.db.recommendations.update({ submitTime: futureSubmitTime });
|
||
|
||
server.create('job', {
|
||
name: 'oooooo',
|
||
createRecommendations: true,
|
||
groupsCount: 2,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
server.create('job', {
|
||
name: 'pppppp',
|
||
createRecommendations: true,
|
||
groupsCount: 2,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
await Optimize.visit();
|
||
|
||
assert.equal(Optimize.card.slug.jobName, 'zzzzzz');
|
||
|
||
assert.equal(
|
||
collapseWhitespace(Optimize.search.placeholder),
|
||
`Search ${Optimize.recommendationSummaries.length} recommendations...`
|
||
);
|
||
|
||
await Optimize.search.fillIn('ooo');
|
||
|
||
assert.equal(Optimize.recommendationSummaries.length, 2);
|
||
assert.ok(Optimize.recommendationSummaries[0].slug.startsWith('oooooo'));
|
||
|
||
assert.equal(Optimize.card.slug.jobName, 'oooooo');
|
||
assert.ok(currentURL().includes('oooooo'));
|
||
|
||
await Optimize.search.fillIn('qqq');
|
||
|
||
assert.notOk(Optimize.card.isPresent);
|
||
assert.ok(Optimize.empty.isPresent);
|
||
assert.equal(Optimize.empty.headline, 'No Matches');
|
||
assert.equal(currentURL(), '/optimize?search=qqq');
|
||
|
||
await Optimize.search.fillIn('');
|
||
|
||
assert.equal(Optimize.card.slug.jobName, 'zzzzzz');
|
||
assert.ok(Optimize.recommendationSummaries[0].isActive);
|
||
});
|
||
|
||
test('the namespaces toggle doesn’t show when there aren’t namespaces', async function (assert) {
|
||
server.db.namespaces.remove();
|
||
|
||
server.create('job', {
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
await Optimize.visit();
|
||
|
||
assert.ok(Optimize.facets.namespace.isHidden);
|
||
});
|
||
|
||
test('processing a summary moves to the next one in the sorted list', async function (assert) {
|
||
server.create('job', {
|
||
name: 'ooo111',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
server.create('job', {
|
||
name: 'pppppp',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
server.create('job', {
|
||
name: 'ooo222',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 4,
|
||
});
|
||
|
||
// Directly set the sorting of the above jobs’s summaries in the table
|
||
const futureSubmitTime = (Date.now() + 10000) * 1000000;
|
||
const nowSubmitTime = Date.now() * 1000000;
|
||
const pastSubmitTime = (Date.now() - 10000) * 1000000;
|
||
|
||
const jobNameToRecommendationSubmitTime = {
|
||
ooo111: futureSubmitTime,
|
||
pppppp: nowSubmitTime,
|
||
ooo222: pastSubmitTime,
|
||
};
|
||
|
||
server.schema.recommendations.all().models.forEach((recommendation) => {
|
||
const parentJob = recommendation.task.taskGroup.job;
|
||
const submitTimeForJob =
|
||
jobNameToRecommendationSubmitTime[parentJob.name];
|
||
recommendation.submitTime = submitTimeForJob;
|
||
recommendation.save();
|
||
});
|
||
|
||
await Optimize.visit();
|
||
await Optimize.search.fillIn('ooo');
|
||
await Optimize.card.acceptButton.click();
|
||
|
||
assert.equal(Optimize.card.slug.jobName, 'ooo222');
|
||
});
|
||
|
||
test('the optimize page has appropriate faceted search options', async function (assert) {
|
||
server.createList('job', 4, {
|
||
status: 'running',
|
||
createRecommendations: true,
|
||
childrenCount: 0,
|
||
});
|
||
|
||
await Optimize.visit();
|
||
|
||
assert.ok(Optimize.facets.namespace.isPresent, 'Namespace facet found');
|
||
assert.ok(Optimize.facets.type.isPresent, 'Type facet found');
|
||
assert.ok(Optimize.facets.status.isPresent, 'Status facet found');
|
||
assert.ok(Optimize.facets.datacenter.isPresent, 'Datacenter facet found');
|
||
assert.ok(Optimize.facets.prefix.isPresent, 'Prefix facet found');
|
||
});
|
||
|
||
testSingleSelectFacet('Namespace', {
|
||
facet: Optimize.facets.namespace,
|
||
paramName: 'namespace',
|
||
expectedOptions: ['All (*)', 'default', 'namespace-1'],
|
||
optionToSelect: 'namespace-1',
|
||
async beforeEach() {
|
||
server.createList('job', 2, {
|
||
namespaceId: 'default',
|
||
createRecommendations: true,
|
||
});
|
||
server.createList('job', 2, {
|
||
namespaceId: 'namespace-1',
|
||
createRecommendations: true,
|
||
});
|
||
await Optimize.visit();
|
||
},
|
||
filter(taskGroup, selection) {
|
||
return taskGroup.job.namespaceId === selection;
|
||
},
|
||
});
|
||
|
||
testFacet('Type', {
|
||
facet: Optimize.facets.type,
|
||
paramName: 'type',
|
||
expectedOptions: ['Service', 'System'],
|
||
async beforeEach() {
|
||
server.createList('job', 2, {
|
||
type: 'service',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
});
|
||
|
||
server.createList('job', 2, {
|
||
type: 'system',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
});
|
||
await Optimize.visit();
|
||
},
|
||
filter(taskGroup, selection) {
|
||
let displayType = taskGroup.job.type;
|
||
return selection.includes(displayType);
|
||
},
|
||
});
|
||
|
||
testFacet('Status', {
|
||
facet: Optimize.facets.status,
|
||
paramName: 'status',
|
||
expectedOptions: ['Pending', 'Running', 'Dead'],
|
||
async beforeEach() {
|
||
server.createList('job', 2, {
|
||
status: 'pending',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.createList('job', 2, {
|
||
status: 'running',
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.createList('job', 2, {
|
||
status: 'dead',
|
||
createRecommendations: true,
|
||
childrenCount: 0,
|
||
});
|
||
await Optimize.visit();
|
||
},
|
||
filter: (taskGroup, selection) => selection.includes(taskGroup.job.status),
|
||
});
|
||
|
||
testFacet('Datacenter', {
|
||
facet: Optimize.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'],
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.create('job', {
|
||
datacenters: ['pdx', 'ord'],
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.create('job', {
|
||
datacenters: ['lax', 'jfk'],
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.create('job', {
|
||
datacenters: ['jfk', 'dfw'],
|
||
createRecommendations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
server.create('job', {
|
||
datacenters: ['pdx'],
|
||
createRecommendations: true,
|
||
childrenCount: 0,
|
||
});
|
||
await Optimize.visit();
|
||
},
|
||
filter: (taskGroup, selection) =>
|
||
taskGroup.job.datacenters.find((dc) => selection.includes(dc)),
|
||
});
|
||
|
||
testFacet('Prefix', {
|
||
facet: Optimize.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,
|
||
createRecommendations: true,
|
||
createAllocations: true,
|
||
groupsCount: 1,
|
||
groupTaskCount: 2,
|
||
childrenCount: 0,
|
||
});
|
||
});
|
||
await Optimize.visit();
|
||
},
|
||
filter: (taskGroup, selection) =>
|
||
selection.find((prefix) => taskGroup.job.name.startsWith(prefix)),
|
||
});
|
||
|
||
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.call(this, 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 sortedRecommendations = server.db.recommendations
|
||
.sortBy('submitTime')
|
||
.reverse();
|
||
|
||
const recommendationTaskGroups = server.schema.tasks
|
||
.find(sortedRecommendations.mapBy('taskId').uniq())
|
||
.models.mapBy('taskGroup')
|
||
.uniqBy('id')
|
||
.filter((group) => filter(group, selection));
|
||
|
||
Optimize.recommendationSummaries.forEach((summary, index) => {
|
||
const group = recommendationTaskGroups[index];
|
||
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
|
||
});
|
||
});
|
||
|
||
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.call(this, assert, beforeEach, facet, expectedOptions);
|
||
});
|
||
|
||
test(`the ${label} facet filters the recommendation summaries 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 sortedRecommendations = server.db.recommendations
|
||
.sortBy('submitTime')
|
||
.reverse();
|
||
|
||
const recommendationTaskGroups = server.schema.tasks
|
||
.find(sortedRecommendations.mapBy('taskId').uniq())
|
||
.models.mapBy('taskGroup')
|
||
.uniqBy('id')
|
||
.filter((group) => filter(group, selection));
|
||
|
||
Optimize.recommendationSummaries.forEach((summary, index) => {
|
||
const group = recommendationTaskGroups[index];
|
||
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
|
||
});
|
||
});
|
||
|
||
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 sortedRecommendations = server.db.recommendations
|
||
.sortBy('submitTime')
|
||
.reverse();
|
||
|
||
const recommendationTaskGroups = server.schema.tasks
|
||
.find(sortedRecommendations.mapBy('taskId').uniq())
|
||
.models.mapBy('taskGroup')
|
||
.uniqBy('id')
|
||
.filter((group) => filter(group, selection));
|
||
|
||
Optimize.recommendationSummaries.forEach((summary, index) => {
|
||
const group = recommendationTaskGroups[index];
|
||
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
|
||
});
|
||
});
|
||
|
||
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)))
|
||
);
|
||
});
|
||
}
|
||
});
|
||
|
||
function formattedMemDiff(memDiff) {
|
||
const absMemDiff = Math.abs(memDiff);
|
||
const negativeSign = memDiff < 0 ? '-' : '';
|
||
|
||
return negativeSign + formatBytes(absMemDiff, 'MiB');
|
||
}
|