github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/evaluations-test.js (about) 1 /* eslint-disable qunit/require-expect */ 2 import { 3 click, 4 currentRouteName, 5 currentURL, 6 typeIn, 7 visit, 8 waitFor, 9 waitUntil, 10 } from '@ember/test-helpers'; 11 import { module, test } from 'qunit'; 12 import { setupApplicationTest } from 'ember-qunit'; 13 import { setupMirage } from 'ember-cli-mirage/test-support'; 14 import { Response } from 'ember-cli-mirage'; 15 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 16 import { 17 selectChoose, 18 clickTrigger, 19 } from 'ember-power-select/test-support/helpers'; 20 import { generateAcceptanceTestEvalMock } from '../../mirage/utils'; 21 import percySnapshot from '@percy/ember'; 22 import faker from 'nomad-ui/mirage/faker'; 23 24 const getStandardRes = () => [ 25 { 26 CreateIndex: 1249, 27 CreateTime: 1640181894162724000, 28 DeploymentID: '12efbb28-840e-7794-b215-a7b112e40a4f', 29 ID: '5fb1b8cd-00f8-fff8-de0c-197dc37f5053', 30 JobID: 'cores-example', 31 JobModifyIndex: 694, 32 ModifyIndex: 1251, 33 ModifyTime: 1640181894167194000, 34 Namespace: 'ted-lasso', 35 Priority: 50, 36 QueuedAllocations: { 37 lb: 0, 38 webapp: 0, 39 }, 40 SnapshotIndex: 1249, 41 Status: 'complete', 42 TriggeredBy: 'job-register', 43 Type: 'service', 44 }, 45 { 46 CreateIndex: 1304, 47 CreateTime: 1640183201719510000, 48 DeploymentID: '878435bf-7265-62b1-7902-d45c44b23b79', 49 ID: '66cb98a6-7740-d5ef-37e4-fa0f8b1de44b', 50 JobID: 'cores-example', 51 JobModifyIndex: 1304, 52 ModifyIndex: 1306, 53 ModifyTime: 1640183201721418000, 54 Namespace: 'default', 55 Priority: 50, 56 QueuedAllocations: { 57 webapp: 0, 58 lb: 0, 59 }, 60 SnapshotIndex: 1304, 61 Status: 'complete', 62 TriggeredBy: 'job-register', 63 Type: 'service', 64 }, 65 { 66 CreateIndex: 1267, 67 CreateTime: 1640182198255685000, 68 DeploymentID: '12efbb28-840e-7794-b215-a7b112e40a4f', 69 ID: '78009518-574d-eee6-919a-e83879175dd3', 70 JobID: 'cores-example', 71 JobModifyIndex: 1250, 72 ModifyIndex: 1274, 73 ModifyTime: 1640182228112823000, 74 Namespace: 'ted-lasso', 75 PreviousEval: '84f1082f-3e6e-034d-6df4-c6a321e7bd63', 76 Priority: 50, 77 QueuedAllocations: { 78 lb: 0, 79 }, 80 SnapshotIndex: 1272, 81 Status: 'complete', 82 TriggeredBy: 'alloc-failure', 83 Type: 'service', 84 WaitUntil: '2021-12-22T14:10:28.108136Z', 85 }, 86 { 87 CreateIndex: 1322, 88 CreateTime: 1640183505760099000, 89 DeploymentID: '878435bf-7265-62b1-7902-d45c44b23b79', 90 ID: 'c184f72b-68a3-5180-afd6-af01860ad371', 91 JobID: 'cores-example', 92 JobModifyIndex: 1305, 93 ModifyIndex: 1329, 94 ModifyTime: 1640183535540881000, 95 Namespace: 'default', 96 PreviousEval: '9a917a93-7bc3-6991-ffc9-15919a38f04b', 97 Priority: 50, 98 QueuedAllocations: { 99 lb: 0, 100 }, 101 SnapshotIndex: 1326, 102 Status: 'complete', 103 TriggeredBy: 'alloc-failure', 104 Type: 'service', 105 WaitUntil: '2021-12-22T14:32:15.539556Z', 106 }, 107 ]; 108 109 module('Acceptance | evaluations list', function (hooks) { 110 setupApplicationTest(hooks); 111 setupMirage(hooks); 112 113 test('it passes an accessibility audit', async function (assert) { 114 assert.expect(2); 115 116 await visit('/evaluations'); 117 118 assert.equal( 119 currentRouteName(), 120 'evaluations.index', 121 'The default route in evaluations is evaluations index' 122 ); 123 124 await a11yAudit(assert); 125 }); 126 127 test('it renders an empty message if there are no evaluations rendered', async function (assert) { 128 faker.seed(1); 129 130 await visit('/evaluations'); 131 assert.expect(2); 132 133 await percySnapshot(assert); 134 135 assert 136 .dom('[data-test-empty-evaluations-list]') 137 .exists('We display empty table message.'); 138 assert 139 .dom('[data-test-no-eval]') 140 .exists('We display a message saying there are no evaluations.'); 141 }); 142 143 test('it renders a list of evaluations', async function (assert) { 144 faker.seed(1); 145 assert.expect(3); 146 server.get('/evaluations', function (_server, fakeRequest) { 147 assert.deepEqual( 148 fakeRequest.queryParams, 149 { 150 namespace: '*', 151 per_page: '25', 152 next_token: '', 153 filter: '', 154 reverse: 'true', 155 }, 156 'Forwards the correct query parameters on default query when route initially loads' 157 ); 158 return getStandardRes(); 159 }); 160 161 await visit('/evaluations'); 162 163 await percySnapshot(assert); 164 165 assert 166 .dom('[data-test-eval-table]') 167 .exists('Evaluations table should render'); 168 assert 169 .dom('[data-test-evaluation]') 170 .exists({ count: 4 }, 'Should render the correct number of evaluations'); 171 }); 172 173 module('filters', function () { 174 test('it should enable filtering by evaluation status', async function (assert) { 175 assert.expect(2); 176 177 server.get('/evaluations', getStandardRes); 178 179 await visit('/evaluations'); 180 181 server.get('/evaluations', function (_server, fakeRequest) { 182 assert.deepEqual( 183 fakeRequest.queryParams, 184 { 185 namespace: '*', 186 per_page: '25', 187 next_token: '', 188 filter: 'Status contains "pending"', 189 reverse: 'true', 190 }, 191 'It makes another server request using the options selected by the user' 192 ); 193 return []; 194 }); 195 196 await clickTrigger('[data-test-evaluation-status-facet]'); 197 await selectChoose('[data-test-evaluation-status-facet]', 'Pending'); 198 199 assert 200 .dom('[data-test-no-eval-match]') 201 .exists('Renders a message saying no evaluations match filter status'); 202 }); 203 204 test('it should enable filtering by namespace', async function (assert) { 205 assert.expect(2); 206 207 server.get('/evaluations', getStandardRes); 208 209 await visit('/evaluations'); 210 211 server.get('/evaluations', function (_server, fakeRequest) { 212 assert.deepEqual( 213 fakeRequest.queryParams, 214 { 215 namespace: 'default', 216 per_page: '25', 217 next_token: '', 218 filter: '', 219 reverse: 'true', 220 }, 221 'It makes another server request using the options selected by the user' 222 ); 223 return []; 224 }); 225 226 await clickTrigger('[data-test-evaluation-namespace-facet]'); 227 await selectChoose('[data-test-evaluation-namespace-facet]', 'default'); 228 229 assert 230 .dom('[data-test-empty-evaluations-list]') 231 .exists('Renders a message saying no evaluations match filter status'); 232 }); 233 234 test('it should enable filtering by triggered by', async function (assert) { 235 assert.expect(2); 236 237 server.get('/evaluations', getStandardRes); 238 239 await visit('/evaluations'); 240 241 server.get('/evaluations', function (_server, fakeRequest) { 242 assert.deepEqual( 243 fakeRequest.queryParams, 244 { 245 namespace: '*', 246 per_page: '25', 247 next_token: '', 248 filter: `TriggeredBy contains "periodic-job"`, 249 reverse: 'true', 250 }, 251 'It makes another server request using the options selected by the user' 252 ); 253 return []; 254 }); 255 256 await clickTrigger('[data-test-evaluation-triggered-by-facet]'); 257 await selectChoose( 258 '[data-test-evaluation-triggered-by-facet]', 259 'Periodic Job' 260 ); 261 262 assert 263 .dom('[data-test-empty-evaluations-list]') 264 .exists('Renders a message saying no evaluations match filter status'); 265 }); 266 267 test('it should enable filtering by type', async function (assert) { 268 assert.expect(2); 269 270 server.get('/evaluations', getStandardRes); 271 272 await visit('/evaluations'); 273 274 server.get('/evaluations', function (_server, fakeRequest) { 275 assert.deepEqual( 276 fakeRequest.queryParams, 277 { 278 namespace: '*', 279 per_page: '25', 280 next_token: '', 281 filter: 'NodeID is not empty', 282 reverse: 'true', 283 }, 284 'It makes another server request using the options selected by the user' 285 ); 286 return []; 287 }); 288 289 await clickTrigger('[data-test-evaluation-type-facet]'); 290 await selectChoose('[data-test-evaluation-type-facet]', 'Client'); 291 292 assert 293 .dom('[data-test-empty-evaluations-list]') 294 .exists('Renders a message saying no evaluations match filter status'); 295 }); 296 297 test('it should enable filtering by search term', async function (assert) { 298 assert.expect(2); 299 300 server.get('/evaluations', getStandardRes); 301 302 await visit('/evaluations'); 303 304 const searchTerm = 'Lasso'; 305 server.get('/evaluations', function (_server, fakeRequest) { 306 assert.deepEqual( 307 fakeRequest.queryParams, 308 { 309 namespace: '*', 310 per_page: '25', 311 next_token: '', 312 filter: `ID contains "${searchTerm}" or JobID contains "${searchTerm}" or NodeID contains "${searchTerm}" or TriggeredBy contains "${searchTerm}"`, 313 reverse: 'true', 314 }, 315 'It makes another server request using the options selected by the user' 316 ); 317 return []; 318 }); 319 320 await typeIn('[data-test-evaluations-search] input', searchTerm); 321 322 assert 323 .dom('[data-test-empty-evaluations-list]') 324 .exists('Renders a message saying no evaluations match filter status'); 325 }); 326 327 test('it should enable combining filters and search', async function (assert) { 328 assert.expect(5); 329 330 server.get('/evaluations', getStandardRes); 331 332 await visit('/evaluations'); 333 334 const searchTerm = 'Lasso'; 335 server.get('/evaluations', function (_server, fakeRequest) { 336 assert.deepEqual( 337 fakeRequest.queryParams, 338 { 339 namespace: '*', 340 per_page: '25', 341 next_token: '', 342 filter: `ID contains "${searchTerm}" or JobID contains "${searchTerm}" or NodeID contains "${searchTerm}" or TriggeredBy contains "${searchTerm}"`, 343 reverse: 'true', 344 }, 345 'It makes another server request using the options selected by the user' 346 ); 347 return []; 348 }); 349 await typeIn('[data-test-evaluations-search] input', searchTerm); 350 351 server.get('/evaluations', function (_server, fakeRequest) { 352 assert.deepEqual( 353 fakeRequest.queryParams, 354 { 355 namespace: '*', 356 per_page: '25', 357 next_token: '', 358 filter: `(ID contains "${searchTerm}" or JobID contains "${searchTerm}" or NodeID contains "${searchTerm}" or TriggeredBy contains "${searchTerm}") and NodeID is not empty`, 359 reverse: 'true', 360 }, 361 'It makes another server request using the options selected by the user' 362 ); 363 return []; 364 }); 365 await clickTrigger('[data-test-evaluation-type-facet]'); 366 await selectChoose('[data-test-evaluation-type-facet]', 'Client'); 367 368 server.get('/evaluations', function (_server, fakeRequest) { 369 assert.deepEqual( 370 fakeRequest.queryParams, 371 { 372 namespace: '*', 373 per_page: '25', 374 next_token: '', 375 filter: `NodeID is not empty`, 376 reverse: 'true', 377 }, 378 'It makes another server request using the options selected by the user' 379 ); 380 return []; 381 }); 382 await click('[data-test-evaluations-search] button'); 383 384 server.get('/evaluations', function (_server, fakeRequest) { 385 assert.deepEqual( 386 fakeRequest.queryParams, 387 { 388 namespace: '*', 389 per_page: '25', 390 next_token: '', 391 filter: `NodeID is not empty and Status contains "complete"`, 392 reverse: 'true', 393 }, 394 'It makes another server request using the options selected by the user' 395 ); 396 return []; 397 }); 398 await clickTrigger('[data-test-evaluation-status-facet]'); 399 await selectChoose('[data-test-evaluation-status-facet]', 'Complete'); 400 401 assert 402 .dom('[data-test-empty-evaluations-list]') 403 .exists('Renders a message saying no evaluations match filter status'); 404 }); 405 }); 406 407 module('page size', function (hooks) { 408 hooks.afterEach(function () { 409 // PageSizeSelect and the Evaluations Controller are both using localStorage directly 410 // Will come back and invert the dependency 411 window.localStorage.clear(); 412 }); 413 414 test('it is possible to change page size', async function (assert) { 415 assert.expect(1); 416 417 server.get('/evaluations', getStandardRes); 418 419 await visit('/evaluations'); 420 421 server.get('/evaluations', function (_server, fakeRequest) { 422 assert.deepEqual( 423 fakeRequest.queryParams, 424 { 425 namespace: '*', 426 per_page: '50', 427 next_token: '', 428 filter: '', 429 reverse: 'true', 430 }, 431 'It makes a request with the per_page set by the user' 432 ); 433 return getStandardRes(); 434 }); 435 436 await clickTrigger('[data-test-per-page]'); 437 await selectChoose('[data-test-per-page]', 50); 438 }); 439 }); 440 441 module('pagination', function () { 442 test('it should enable pagination by using next tokens', async function (assert) { 443 assert.expect(7); 444 445 server.get('/evaluations', function () { 446 return new Response( 447 200, 448 { 'x-nomad-nexttoken': 'next-token-1' }, 449 getStandardRes() 450 ); 451 }); 452 453 await visit('/evaluations'); 454 455 server.get('/evaluations', function (_server, fakeRequest) { 456 assert.deepEqual( 457 fakeRequest.queryParams, 458 { 459 namespace: '*', 460 per_page: '25', 461 next_token: 'next-token-1', 462 filter: '', 463 reverse: 'true', 464 }, 465 'It makes another server request using the options selected by the user' 466 ); 467 return new Response( 468 200, 469 { 'x-nomad-nexttoken': 'next-token-2' }, 470 getStandardRes() 471 ); 472 }); 473 474 assert 475 .dom('[data-test-eval-pagination-next]') 476 .isEnabled( 477 'If there is a next-token in the API response the next button should be enabled.' 478 ); 479 await click('[data-test-eval-pagination-next]'); 480 481 server.get('/evaluations', function (_server, fakeRequest) { 482 assert.deepEqual( 483 fakeRequest.queryParams, 484 { 485 namespace: '*', 486 per_page: '25', 487 next_token: 'next-token-2', 488 filter: '', 489 reverse: 'true', 490 }, 491 'It makes another server request using the options selected by the user' 492 ); 493 return getStandardRes(); 494 }); 495 await click('[data-test-eval-pagination-next]'); 496 497 assert 498 .dom('[data-test-eval-pagination-next]') 499 .isDisabled('If there is no next-token, the next button is disabled.'); 500 501 assert 502 .dom('[data-test-eval-pagination-prev]') 503 .isEnabled( 504 'After we transition to the next page, the previous page button is enabled.' 505 ); 506 507 server.get('/evaluations', function (_server, fakeRequest) { 508 assert.deepEqual( 509 fakeRequest.queryParams, 510 { 511 namespace: '*', 512 per_page: '25', 513 next_token: 'next-token-1', 514 filter: '', 515 reverse: 'true', 516 }, 517 'It makes a request using the stored old token.' 518 ); 519 return new Response( 520 200, 521 { 'x-nomad-nexttoken': 'next-token-2' }, 522 getStandardRes() 523 ); 524 }); 525 526 await click('[data-test-eval-pagination-prev]'); 527 528 server.get('/evaluations', function (_server, fakeRequest) { 529 assert.deepEqual( 530 fakeRequest.queryParams, 531 { 532 namespace: '*', 533 per_page: '25', 534 next_token: '', 535 filter: '', 536 reverse: 'true', 537 }, 538 'When there are no more stored previous tokens, we will request with no next-token.' 539 ); 540 return new Response( 541 200, 542 { 'x-nomad-nexttoken': 'next-token-1' }, 543 getStandardRes() 544 ); 545 }); 546 547 await click('[data-test-eval-pagination-prev]'); 548 }); 549 550 test('it should clear all query parameters on refresh', async function (assert) { 551 assert.expect(1); 552 553 server.get('/evaluations', function () { 554 return new Response( 555 200, 556 { 'x-nomad-nexttoken': 'next-token-1' }, 557 getStandardRes() 558 ); 559 }); 560 561 await visit('/evaluations'); 562 563 server.get('/evaluations', function () { 564 return getStandardRes(); 565 }); 566 567 await click('[data-test-eval-pagination-next]'); 568 569 await clickTrigger('[data-test-evaluation-status-facet]'); 570 await selectChoose('[data-test-evaluation-status-facet]', 'Pending'); 571 572 server.get('/evaluations', function (_server, fakeRequest) { 573 assert.deepEqual( 574 fakeRequest.queryParams, 575 { 576 namespace: '*', 577 per_page: '25', 578 next_token: '', 579 filter: '', 580 reverse: 'true', 581 }, 582 'It clears all query parameters when making a refresh' 583 ); 584 return new Response( 585 200, 586 { 'x-nomad-nexttoken': 'next-token-1' }, 587 getStandardRes() 588 ); 589 }); 590 591 await click('[data-test-eval-refresh]'); 592 }); 593 594 test('it should reset pagination when filters are applied', async function (assert) { 595 assert.expect(1); 596 597 server.get('/evaluations', function () { 598 return new Response( 599 200, 600 { 'x-nomad-nexttoken': 'next-token-1' }, 601 getStandardRes() 602 ); 603 }); 604 605 await visit('/evaluations'); 606 607 server.get('/evaluations', function () { 608 return new Response( 609 200, 610 { 'x-nomad-nexttoken': 'next-token-2' }, 611 getStandardRes() 612 ); 613 }); 614 615 await click('[data-test-eval-pagination-next]'); 616 617 server.get('/evaluations', getStandardRes); 618 await click('[data-test-eval-pagination-next]'); 619 620 server.get('/evaluations', function (_server, fakeRequest) { 621 assert.deepEqual( 622 fakeRequest.queryParams, 623 { 624 namespace: '*', 625 per_page: '25', 626 next_token: '', 627 filter: 'Status contains "pending"', 628 reverse: 'true', 629 }, 630 'It clears all next token when filtered request is made' 631 ); 632 return getStandardRes(); 633 }); 634 await clickTrigger('[data-test-evaluation-status-facet]'); 635 await selectChoose('[data-test-evaluation-status-facet]', 'Pending'); 636 }); 637 }); 638 639 module('resource linking', function () { 640 test('it should generate a link to the job resource', async function (assert) { 641 server.create('node'); 642 const job = server.create('job', { id: 'example', shallow: true }); 643 server.create('evaluation', { jobId: job.id }); 644 645 await visit('/evaluations'); 646 assert 647 .dom('[data-test-evaluation-resource]') 648 .hasText( 649 job.name, 650 'It conditionally renders the correct resource name' 651 ); 652 653 await click('[data-test-evaluation-resource]'); 654 assert 655 .dom('[data-test-job-name]') 656 .includesText(job.name, 'We navigate to the correct job page.'); 657 }); 658 659 test('it should generate a link to the node resource', async function (assert) { 660 const node = server.create('node'); 661 server.create('evaluation', { nodeId: node.id }); 662 await visit('/evaluations'); 663 664 const shortNodeId = node.id.split('-')[0]; 665 assert 666 .dom('[data-test-evaluation-resource]') 667 .hasText( 668 shortNodeId, 669 'It conditionally renders the correct resource name' 670 ); 671 672 await click('[data-test-evaluation-resource]'); 673 674 assert 675 .dom('[data-test-title]') 676 .includesText(node.name, 'We navigate to the correct client page.'); 677 }); 678 }); 679 680 module('evaluation detail', function () { 681 test('clicking an evaluation opens the detail view', async function (assert) { 682 faker.seed(1); 683 server.get('/evaluations', getStandardRes); 684 server.get('/evaluation/:id', function (_, { queryParams, params }) { 685 const expectedNamespaces = ['default', 'ted-lasso']; 686 assert.notEqual( 687 expectedNamespaces.indexOf(queryParams.namespace), 688 -1, 689 'Eval details request has namespace query param' 690 ); 691 692 return { ...generateAcceptanceTestEvalMock(params.id), ID: params.id }; 693 }); 694 695 await visit('/evaluations'); 696 697 const evalId = '5fb1b8cd'; 698 await click(`[data-test-evaluation='${evalId}']`); 699 700 await percySnapshot(assert); 701 702 assert 703 .dom('[data-test-eval-detail-is-open]') 704 .exists( 705 'A sidebar portal mounts to the dom after clicking an evaluation' 706 ); 707 708 assert 709 .dom('[data-test-rel-eval]') 710 .exists( 711 { count: 12 }, 712 'all related evaluations and the current evaluation are displayed' 713 ); 714 715 click(`[data-test-rel-eval='fd1cd898-d655-c7e4-17f6-a1a2e98b18ef']`); 716 await waitFor('[data-test-eval-loading]'); 717 assert 718 .dom('[data-test-eval-loading]') 719 .exists( 720 'transition to loading state after clicking related evaluation' 721 ); 722 723 await waitFor('[data-test-eval-detail-header]'); 724 725 assert.equal( 726 currentURL(), 727 '/evaluations?currentEval=fd1cd898-d655-c7e4-17f6-a1a2e98b18ef' 728 ); 729 assert 730 .dom('[data-test-title]') 731 .includesText('fd1cd898', 'New evaluation hash appears in the title'); 732 733 await click(`[data-test-evaluation='66cb98a6']`); 734 assert.equal( 735 currentURL(), 736 '/evaluations?currentEval=66cb98a6-7740-d5ef-37e4-fa0f8b1de44b', 737 'Clicking an evaluation in the table updates the sidebar' 738 ); 739 740 click('[data-test-eval-sidebar-x]'); 741 742 // We wait until the sidebar closes since it uses a transition of 300ms 743 await waitUntil( 744 () => !document.querySelector('[data-test-eval-detail-is-open]') 745 ); 746 747 assert.equal( 748 currentURL(), 749 '/evaluations', 750 'When the user clicks the x button the sidebar closes' 751 ); 752 }); 753 754 test('it should provide an error state when loading an invalid evaluation', async function (assert) { 755 server.get('/evaluations', getStandardRes); 756 server.get('/evaluation/:id', function () { 757 return new Response(404, {}, ''); 758 }); 759 760 await visit('/evaluations'); 761 762 const evalId = '5fb1b8cd'; 763 await click(`[data-test-evaluation='${evalId}']`); 764 765 assert 766 .dom('[data-test-eval-detail-is-open]') 767 .exists( 768 'A sidebar portal mounts to the dom after clicking an evaluation' 769 ); 770 771 assert 772 .dom('[data-test-eval-error]') 773 .exists( 774 'all related evaluations and the current evaluation are displayed' 775 ); 776 777 click('[data-test-eval-sidebar-x]'); 778 779 // We wait until the sidebar closes since it uses a transition of 300ms 780 await waitUntil( 781 () => !document.querySelector('[data-test-eval-detail-is-open]') 782 ); 783 784 assert.equal( 785 currentURL(), 786 '/evaluations', 787 'When the user clicks the x button the sidebar closes' 788 ); 789 }); 790 }); 791 });