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  });