github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/evaluations-test.js (about)

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