Files
nomad/ui/tests/acceptance/search-test.js
Phil Renaud 1976202cd6 Feature: Dynamic Host Volumes in the UI (#25224)
* DHV UI init

* /csi routes to /storage routes and a routeRedirector util (#25163)

* /csi routes to /storage routes and a routeRedirector util

* Tests and routes move csi/ to storage/

* Changelog added

* [ui] Storage UI overhaul + Dynamic Host Volumes UI (#25226)

* Storage index page and DHV model properties

* Naive version of a storage overview page

* Experimental fetch of alloc data dirs

* Fetch ephemeral disks and static host volumes as an ember concurrency task and nice table stylings

* Playing nice with section header labels to make eslint happy even though wcag was already cool with it

* inlined the storage type explainers and reordered things, plus tooltips and keynav

* Bones of a dynamic host volume individual page

* Woooo dynamic host volume model, adapter, and serializer with embedded alloc relationships

* Couple test fixes

* async:false relationship for dhv.hasMany('alloc') to prevent a ton of xhr requests

* DHV request type at index routemodel and better serialization

* Pagination and searching and query params oh my

* Test retrofits for csi volumes

* Really fantastic flake gets fixed

* DHV detail page acceptance test and a bunch of mirage hooks

* Seed so that the actions test has a guaranteed task

* removed ephemeral disk and static host volume manual scanning

* CapacityBytes and capabilities table added to DHV detail page

* Debugging actions flyout test

* was becoming clear that faker.seed editing was causing havoc elsewhere so might as well not boil the ocean and just tell this test to do what I want it to

* Post-create job gets taskCount instead of count

* CSI volumes now get /csi route prefix at detail level

* lazyclick method for unused keynav removed

* keyboard nav and table-watcher for DHV added

* Addressed PR comments, changed up capabilities table and id references, etc.

* Capabilities table for DHV and ID in details header

* Testfixes for pluginID and capabilities table on DHV page
2025-03-10 14:46:02 -04:00

261 lines
8.0 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
/* eslint-disable ember-a11y-testing/a11y-audit-called */
/* eslint-disable qunit/require-expect */
import { module, test } from 'qunit';
import { currentURL, triggerEvent, visit } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import Layout from 'nomad-ui/tests/pages/layout';
import JobsList from 'nomad-ui/tests/pages/jobs/list';
import { selectSearch } from 'ember-power-select/test-support';
import Response from 'ember-cli-mirage/response';
module('Acceptance | search', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
test('search exposes and navigates to results from the fuzzy search endpoint', async function (assert) {
server.create('node-pool');
server.create('node', { name: 'xyz' });
const otherNode = server.create('node', { name: 'ghi' });
server.create('namespace');
server.create('namespace', { id: 'dev' });
server.create('job', {
id: 'vwxyz',
namespaceId: 'default',
groupsCount: 1,
groupAllocCount: 1,
});
server.create('job', {
id: 'xyz',
name: 'xyz job',
namespaceId: 'default',
groupsCount: 1,
groupAllocCount: 1,
});
server.create('job', {
id: 'xyzw',
name: 'xyzw job',
namespaceId: 'dev',
groupsCount: 1,
groupAllocCount: 1,
});
server.create('job', {
id: 'abc',
namespaceId: 'default',
groupsCount: 1,
groupAllocCount: 1,
});
const firstAllocation = server.schema.allocations.all().models[0];
const firstTaskGroup = server.schema.taskGroups.all().models[0];
const namespacedTaskGroup = server.schema.taskGroups.all().models[2];
server.create('csi-plugin', { id: 'xyz-plugin', createVolumes: false });
await visit('/');
await selectSearch(Layout.navbar.search.scope, 'xy');
Layout.navbar.search.as((search) => {
assert.equal(search.groups.length, 5);
search.groups[0].as((jobs) => {
assert.equal(jobs.name, 'Jobs (3)');
assert.equal(jobs.options.length, 3);
assert.equal(jobs.options[0].text, 'default > vwxyz');
assert.equal(jobs.options[1].text, 'default > xyz job');
assert.equal(jobs.options[2].text, 'dev > xyzw job');
});
search.groups[1].as((clients) => {
assert.equal(clients.name, 'Clients (1)');
assert.equal(clients.options.length, 1);
assert.equal(clients.options[0].text, 'xyz');
});
search.groups[2].as((allocs) => {
assert.equal(allocs.name, 'Allocations (0)');
assert.equal(allocs.options.length, 0);
});
search.groups[3].as((groups) => {
assert.equal(groups.name, 'Task Groups (0)');
assert.equal(groups.options.length, 0);
});
search.groups[4].as((plugins) => {
assert.equal(plugins.name, 'CSI Plugins (1)');
assert.equal(plugins.options.length, 1);
assert.equal(plugins.options[0].text, 'xyz-plugin');
});
});
await Layout.navbar.search.groups[0].options[1].click();
assert.equal(currentURL(), '/jobs/xyz@default');
await selectSearch(Layout.navbar.search.scope, 'xy');
await Layout.navbar.search.groups[0].options[2].click();
assert.equal(currentURL(), '/jobs/xyzw@dev');
await selectSearch(Layout.navbar.search.scope, otherNode.name);
await Layout.navbar.search.groups[1].options[0].click();
assert.equal(currentURL(), `/clients/${otherNode.id}`);
await selectSearch(Layout.navbar.search.scope, firstAllocation.name);
assert.equal(
Layout.navbar.search.groups[2].options[0].text,
`${firstAllocation.namespace} > ${firstAllocation.name}`
);
await Layout.navbar.search.groups[2].options[0].click();
assert.equal(currentURL(), `/allocations/${firstAllocation.id}`);
await selectSearch(Layout.navbar.search.scope, firstTaskGroup.name);
assert.equal(
Layout.navbar.search.groups[3].options[0].text,
`default > vwxyz > ${firstTaskGroup.name}`
);
await Layout.navbar.search.groups[3].options[0].click();
assert.equal(currentURL(), `/jobs/vwxyz@default/${firstTaskGroup.name}`);
await selectSearch(Layout.navbar.search.scope, namespacedTaskGroup.name);
assert.equal(
Layout.navbar.search.groups[3].options[0].text,
`dev > xyzw > ${namespacedTaskGroup.name}`
);
await Layout.navbar.search.groups[3].options[0].click();
assert.equal(currentURL(), `/jobs/xyzw@dev/${namespacedTaskGroup.name}`);
await selectSearch(Layout.navbar.search.scope, 'xy');
await Layout.navbar.search.groups[4].options[0].click();
assert.equal(currentURL(), '/storage/plugins/xyz-plugin');
const fuzzySearchQueries = server.pretender.handledRequests.filterBy(
'url',
'/v1/search/fuzzy'
);
const featureDetectionQueries = fuzzySearchQueries.filter((request) =>
request.requestBody.includes('feature-detection-query')
);
assert.equal(
featureDetectionQueries.length,
1,
'expect the feature detection query to only run once'
);
const realFuzzySearchQuery = fuzzySearchQueries[1];
assert.deepEqual(JSON.parse(realFuzzySearchQuery.requestBody), {
Context: 'all',
Namespace: '*',
Text: 'xy',
});
});
test('search does not perform a request when only one character has been entered', async function (assert) {
await visit('/');
await selectSearch(Layout.navbar.search.scope, 'q');
assert.ok(Layout.navbar.search.noOptionsShown);
assert.equal(
server.pretender.handledRequests.filterBy('url', '/v1/search/fuzzy')
.length,
1,
'expect the feature detection query'
);
});
test('when fuzzy search is disabled on the server, the search control is hidden', async function (assert) {
server.post('/search/fuzzy', function () {
return new Response(500, {}, '');
});
await visit('/');
assert.ok(Layout.navbar.search.isHidden);
});
test('results are truncated at 10 per group', async function (assert) {
server.create('node-pool');
server.create('node', { name: 'xyz' });
for (let i = 0; i < 11; i++) {
server.create('job', { id: `job-${i}`, namespaceId: 'default' });
}
await visit('/');
await selectSearch(Layout.navbar.search.scope, 'job');
Layout.navbar.search.as((search) => {
search.groups[0].as((jobs) => {
assert.equal(jobs.name, 'Jobs (showing 10 of 11)');
assert.equal(jobs.options.length, 10);
});
});
});
test('server-side truncation is indicated in the group label', async function (assert) {
server.create('node-pool');
server.create('node', { name: 'xyz' });
for (let i = 0; i < 21; i++) {
server.create('job', { id: `job-${i}`, namespaceId: 'default' });
}
await visit('/');
await selectSearch(Layout.navbar.search.scope, 'job');
Layout.navbar.search.as((search) => {
search.groups[0].as((jobs) => {
assert.equal(jobs.name, 'Jobs (showing 10 of 20+)');
});
});
});
test('clicking the search field starts search immediately', async function (assert) {
await visit('/');
assert.notOk(Layout.navbar.search.field.isPresent);
await Layout.navbar.search.click();
assert.ok(Layout.navbar.search.field.isPresent);
});
test('pressing slash starts a search', async function (assert) {
await visit('/');
assert.notOk(Layout.navbar.search.field.isPresent);
await triggerEvent('.page-layout', 'keydown', { key: '/' });
assert.ok(Layout.navbar.search.field.isPresent);
});
test('pressing slash when an input element is focused does not start a search', async function (assert) {
server.create('node-pool');
server.create('node');
server.create('job');
await visit('/');
assert.notOk(Layout.navbar.search.field.isPresent);
await JobsList.search.click();
await JobsList.search.keydown({ key: '/' });
assert.notOk(Layout.navbar.search.field.isPresent);
});
});