open-nomad/ui/tests/acceptance/optimize-test.js
Michael Klein e096a0a5ab
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 12:28:36 -05:00

880 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 doesnt 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 objects 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 jobs 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 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);
});
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 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,
};
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');
}