open-nomad/ui/tests/acceptance/optimize-test.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

898 lines
27 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
/* 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';
Upgrade Ember and friends 3.28 (#12215) * 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
2022-03-08 17:28:36 +00:00
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(
2021-12-28 16:08:12 +00:00
(recommendations, task) =>
recommendations.concat(task.recommendations.models),
[]
);
return Math.max(...recommendations.mapBy('submitTime'));
}
2021-12-28 14:45:20 +00:00
module('Acceptance | optimize', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
2021-12-28 14:45:20 +00:00
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;
});
2021-12-28 14:45:20 +00:00
test('it passes an accessibility audit', async function (assert) {
await Optimize.visit();
await a11yAudit(assert);
});
2021-12-28 14:45:20 +00:00
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()
2021-12-28 14:45:20 +00:00
.find((r) => r.resource === 'CPU');
2022-07-11 22:06:18 +00:00
const currentTaskGroupHasMemoryRecommendation =
currentTaskGroup.tasks.models
.mapBy('recommendations.models')
.flat()
.find((r) => r.resource === 'MemoryMB');
// 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',
});
}
2022-07-11 22:06:18 +00:00
if (!currentTaskGroupHasMemoryRecommendation) {
const currentTaskGroupTask = currentTaskGroup.tasks.models[0];
this.server.create('recommendation', {
task: currentTaskGroupTask,
resource: 'MemoryMB',
});
}
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}`
);
2021-12-28 16:08:12 +00:00
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(
2021-12-28 16:08:12 +00:00
(recommendations, task) =>
recommendations.concat(task.recommendations.models),
[]
);
2021-12-28 16:08:12 +00:00
const latestSubmitTime = Math.max(
...currentRecommendations.mapBy('submitTime')
);
2021-12-28 14:45:20 +00:00
Optimize.recommendationSummaries[0].as((summary) => {
assert.equal(
summary.date,
2021-12-28 16:08:12 +00:00
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),
2021-12-28 16:08:12 +00:00
cpuDiff
? `${cpuSign}${formatHertz(
cpuDiff,
'MHz'
)} ${cpuSign}${cpuDiffPercent}%`
: ''
);
assert.equal(
replaceMinus(summary.memory),
2021-12-28 16:08:12 +00:00
memDiff
? `${memSign}${formattedMemDiff(
memDiff
)} ${memSign}${memDiffPercent}%`
: ''
);
assert.equal(
replaceMinus(summary.aggregateCpu),
cpuDiff
2021-12-28 16:08:12 +00:00
? `${cpuSign}${formatHertz(
cpuDiff * currentTaskGroupAllocations.length,
'MHz'
)}`
: ''
);
assert.equal(
replaceMinus(summary.aggregateMemory),
2021-12-28 16:08:12 +00:00
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 doesnt affect the summary table diffs'
);
const currentTaskIds = currentTaskGroup.tasks.models.mapBy('id');
2021-12-28 14:45:20 +00:00
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');
2021-12-28 16:08:12 +00:00
const appliedIds = toggledAnything
? cpuRecommendationIds
: memoryRecommendationIds;
const dismissedIds = toggledAnything ? memoryRecommendationIds : [];
await Optimize.card.acceptButton.click();
2021-12-28 16:08:12 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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 =
2021-12-28 16:08:12 +00:00
Optimize.recommendationSummaries[
Optimize.recommendationSummaries.length - 1
];
const collapsedSlug = lastSummary.slug.replace(' / ', '/');
// preferable to use page objects visitable but it encodes the slash
2021-12-28 16:08:12 +00:00
await visit(
`/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`
);
assert.equal(
`${Optimize.card.slug.jobName} / ${Optimize.card.slug.groupName}`,
lastSummary.slug
);
assert.ok(lastSummary.isActive);
2021-12-28 16:08:12 +00:00
assert.equal(
currentURL(),
`/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`
);
});
2021-12-28 14:45:20 +00:00
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');
2021-12-28 16:08:12 +00:00
assert.equal(
currentURL(),
'/optimize/nonexistent/summary?namespace=anamespace'
);
assert.ok(Optimize.applicationError.isPresent);
assert.equal(Optimize.applicationError.title, 'Not Found');
});
2021-12-28 14:45:20 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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');
2021-12-28 14:45:20 +00:00
const taskIdFilter = (task) => currentTaskIds.includes(task.taskId);
const idsBeforeDismissal = server.schema.recommendations
.all()
.models.filter(taskIdFilter)
.mapBy('id');
await Optimize.card.dismissButton.click();
2021-12-28 16:08:12 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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');
});
2021-12-28 14:45:20 +00:00
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);
});
2021-12-28 14:45:20 +00:00
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);
});
2021-12-28 14:45:20 +00:00
test('it reloads partially-loaded jobs', async function (assert) {
await JobsList.visit();
await Optimize.visit();
assert.equal(Optimize.recommendationSummaries.length, 2);
});
});
2021-12-28 14:45:20 +00:00
module('Acceptance | optimize search and facets', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
2021-12-28 14:45:20 +00:00
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;
});
2021-12-28 14:45:20 +00:00
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) {
2020-11-10 19:37:58 +00:00
server.create('job', {
name: 'zzzzzz',
createRecommendations: true,
groupsCount: 1,
groupTaskCount: 6,
});
// Ensure this jobs recommendations are sorted to the top of the table
const futureSubmitTime = (Date.now() + 10000) * 1000000;
server.db.recommendations.update({ submitTime: futureSubmitTime });
2020-11-10 19:37:58 +00:00
server.create('job', {
name: 'oooooo',
createRecommendations: true,
groupsCount: 2,
groupTaskCount: 4,
});
2020-11-10 19:37:58 +00:00
server.create('job', {
name: 'pppppp',
createRecommendations: true,
groupsCount: 2,
groupTaskCount: 4,
});
await Optimize.visit();
assert.equal(Optimize.card.slug.jobName, 'zzzzzz');
assert.equal(
Upgrade Ember and friends 3.28 (#12215) * 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
2022-03-08 17:28:36 +00:00
collapseWhitespace(Optimize.search.placeholder),
`Search ${Optimize.recommendationSummaries.length} recommendations...`
);
2020-11-09 15:32:49 +00:00
await Optimize.search.fillIn('ooo');
assert.equal(Optimize.recommendationSummaries.length, 2);
assert.ok(Optimize.recommendationSummaries[0].slug.startsWith('oooooo'));
2020-11-09 15:41:18 +00:00
assert.equal(Optimize.card.slug.jobName, 'oooooo');
assert.ok(currentURL().includes('oooooo'));
2020-11-09 15:41:18 +00:00
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);
});
2021-12-28 14:45:20 +00:00
test('the namespaces toggle doesnt show when there arent 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);
});
2021-12-28 14:45:20 +00:00
test('processing a summary moves to the next one in the sorted list', async function (assert) {
2020-11-10 19:37:58 +00:00
server.create('job', {
2020-11-09 21:00:58 +00:00
name: 'ooo111',
createRecommendations: true,
groupsCount: 1,
groupTaskCount: 4,
});
2020-11-10 19:37:58 +00:00
server.create('job', {
2020-11-09 21:00:58 +00:00
name: 'pppppp',
createRecommendations: true,
groupsCount: 1,
groupTaskCount: 4,
});
2020-11-10 19:37:58 +00:00
server.create('job', {
2020-11-09 21:00:58 +00:00
name: 'ooo222',
createRecommendations: true,
groupsCount: 1,
groupTaskCount: 4,
});
// Directly set the sorting of the above jobss 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,
};
2021-12-28 14:45:20 +00:00
server.schema.recommendations.all().models.forEach((recommendation) => {
2020-11-09 21:00:58 +00:00
const parentJob = recommendation.task.taskGroup.job;
2021-12-28 16:08:12 +00:00
const submitTimeForJob =
jobNameToRecommendationSubmitTime[parentJob.name];
2020-11-09 21:00:58 +00:00
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');
});
2021-12-28 14:45:20 +00:00
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() {
2021-12-28 16:08:12 +00:00
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,
});
2021-12-28 16:08:12 +00:00
server.create('job', {
datacenters: ['pdx'],
createRecommendations: true,
childrenCount: 0,
});
await Optimize.visit();
},
2021-12-28 14:45:20 +00:00
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',
2021-12-28 14:45:20 +00:00
].forEach((name) => {
server.create('job', {
name,
createRecommendations: true,
createAllocations: true,
groupsCount: 1,
groupTaskCount: 2,
childrenCount: 0,
});
});
await Optimize.visit();
},
filter: (taskGroup, selection) =>
2021-12-28 14:45:20 +00:00
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(
2021-12-28 14:45:20 +00:00
facet.options.map((option) => option.label.trim()),
expectation,
'Options for facet are as expected'
);
}
function testSingleSelectFacet(
label,
{ facet, paramName, beforeEach, filter, expectedOptions, optionToSelect }
) {
2021-12-28 14:45:20 +00:00
test(`the ${label} facet has the correct options`, async function (assert) {
await facetOptions.call(this, assert, beforeEach, facet, expectedOptions);
});
2021-12-28 14:45:20 +00:00
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();
2021-12-28 16:08:12 +00:00
const sortedRecommendations = server.db.recommendations
.sortBy('submitTime')
.reverse();
const recommendationTaskGroups = server.schema.tasks
.find(sortedRecommendations.mapBy('taskId').uniq())
.models.mapBy('taskGroup')
.uniqBy('id')
2021-12-28 14:45:20 +00:00
.filter((group) => filter(group, selection));
Optimize.recommendationSummaries.forEach((summary, index) => {
const group = recommendationTaskGroups[index];
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
});
});
2021-12-28 14:45:20 +00:00
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'
);
});
}
2021-12-28 16:08:12 +00:00
function testFacet(
label,
{ facet, paramName, beforeEach, filter, expectedOptions }
) {
2021-12-28 14:45:20 +00:00
test(`the ${label} facet has the correct options`, async function (assert) {
await facetOptions.call(this, assert, beforeEach, facet, expectedOptions);
});
2021-12-28 14:45:20 +00:00
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];
2021-12-28 16:08:12 +00:00
const sortedRecommendations = server.db.recommendations
.sortBy('submitTime')
.reverse();
const recommendationTaskGroups = server.schema.tasks
.find(sortedRecommendations.mapBy('taskId').uniq())
.models.mapBy('taskGroup')
.uniqBy('id')
2021-12-28 14:45:20 +00:00
.filter((group) => filter(group, selection));
Optimize.recommendationSummaries.forEach((summary, index) => {
const group = recommendationTaskGroups[index];
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
});
});
2021-12-28 14:45:20 +00:00
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);
2021-12-28 16:08:12 +00:00
const sortedRecommendations = server.db.recommendations
.sortBy('submitTime')
.reverse();
const recommendationTaskGroups = server.schema.tasks
.find(sortedRecommendations.mapBy('taskId').uniq())
.models.mapBy('taskGroup')
.uniqBy('id')
2021-12-28 14:45:20 +00:00
.filter((group) => filter(group, selection));
Optimize.recommendationSummaries.forEach((summary, index) => {
const group = recommendationTaskGroups[index];
assert.equal(summary.slug, `${group.job.name} / ${group.name}`);
});
});
2021-12-28 14:45:20 +00:00
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);
2021-12-28 16:08:12 +00:00
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');
}