github.com/blixtra/nomad@v0.7.2-0.20171221000451-da9a1d7bb050/ui/tests/acceptance/job-detail-test.js (about)

     1  import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers';
     2  import Ember from 'ember';
     3  import moment from 'moment';
     4  import { test } from 'qunit';
     5  import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
     6  
     7  const { get, $ } = Ember;
     8  const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
     9  
    10  let job;
    11  
    12  moduleForAcceptance('Acceptance | job detail', {
    13    beforeEach() {
    14      server.create('node');
    15      job = server.create('job', { type: 'service' });
    16      visit(`/jobs/${job.id}`);
    17    },
    18  });
    19  
    20  test('visiting /jobs/:job_id', function(assert) {
    21    assert.equal(currentURL(), `/jobs/${job.id}`);
    22  });
    23  
    24  test('breadcrumbs includes job name and link back to the jobs list', function(assert) {
    25    assert.equal(findAll('.breadcrumb')[0].textContent, 'Jobs', 'First breadcrumb says jobs');
    26    assert.equal(
    27      findAll('.breadcrumb')[1].textContent,
    28      job.name,
    29      'Second breadcrumb says the job name'
    30    );
    31  
    32    click(findAll('.breadcrumb')[0]);
    33    andThen(() => {
    34      assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs');
    35    });
    36  });
    37  
    38  test('the subnav includes links to definition, versions, and deployments when type = service', function(
    39    assert
    40  ) {
    41    const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent);
    42    assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
    43    assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
    44    assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
    45  });
    46  
    47  test('the subnav includes links to definition and versions when type != service', function(assert) {
    48    job = server.create('job', { type: 'batch' });
    49    visit(`/jobs/${job.id}`);
    50  
    51    andThen(() => {
    52      const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent);
    53      assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link');
    54      assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link');
    55      assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link');
    56    });
    57  });
    58  
    59  test('the job detail page should contain basic information about the job', function(assert) {
    60    assert.ok(findAll('.title .tag')[0].textContent.includes(job.status), 'Status');
    61    assert.ok(findAll('.job-stats span')[0].textContent.includes(job.type), 'Type');
    62    assert.ok(findAll('.job-stats span')[1].textContent.includes(job.priority), 'Priority');
    63    assert.notOk(findAll('.job-stats span')[2], 'Namespace is not included');
    64  });
    65  
    66  test('the job detail page should list all task groups', function(assert) {
    67    assert.equal(
    68      findAll('.task-group-row').length,
    69      server.db.taskGroups.where({ jobId: job.id }).length
    70    );
    71  });
    72  
    73  test('each row in the task group table should show basic information about the task group', function(
    74    assert
    75  ) {
    76    const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0];
    77    const taskGroupRow = $(findAll('.task-group-row')[0]);
    78    const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id });
    79    const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0);
    80  
    81    assert.equal(
    82      taskGroupRow
    83        .find('td:eq(0)')
    84        .text()
    85        .trim(),
    86      taskGroup.name,
    87      'Name'
    88    );
    89    assert.equal(
    90      taskGroupRow
    91        .find('td:eq(1)')
    92        .text()
    93        .trim(),
    94      taskGroup.count,
    95      'Count'
    96    );
    97    assert.equal(
    98      taskGroupRow.find('td:eq(3)').text(),
    99      `${sum(tasks, 'Resources.CPU')} MHz`,
   100      'Reserved CPU'
   101    );
   102    assert.equal(
   103      taskGroupRow.find('td:eq(4)').text(),
   104      `${sum(tasks, 'Resources.MemoryMB')} MiB`,
   105      'Reserved Memory'
   106    );
   107    assert.equal(
   108      taskGroupRow.find('td:eq(5)').text(),
   109      `${taskGroup.ephemeralDisk.SizeMB} MiB`,
   110      'Reserved Disk'
   111    );
   112  });
   113  
   114  test('the allocations diagram lists all allocation status figures', function(assert) {
   115    const legend = find('.distribution-bar .legend');
   116    const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id });
   117    const statusCounts = Object.keys(jobSummary.Summary).reduce(
   118      (counts, key) => {
   119        const group = jobSummary.Summary[key];
   120        counts.queued += group.Queued;
   121        counts.starting += group.Starting;
   122        counts.running += group.Running;
   123        counts.complete += group.Complete;
   124        counts.failed += group.Failed;
   125        counts.lost += group.Lost;
   126        return counts;
   127      },
   128      { queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 }
   129    );
   130  
   131    assert.equal(
   132      legend.querySelector('li.queued .value').textContent,
   133      statusCounts.queued,
   134      `${statusCounts.queued} are queued`
   135    );
   136  
   137    assert.equal(
   138      legend.querySelector('li.starting .value').textContent,
   139      statusCounts.starting,
   140      `${statusCounts.starting} are starting`
   141    );
   142  
   143    assert.equal(
   144      legend.querySelector('li.running .value').textContent,
   145      statusCounts.running,
   146      `${statusCounts.running} are running`
   147    );
   148  
   149    assert.equal(
   150      legend.querySelector('li.complete .value').textContent,
   151      statusCounts.complete,
   152      `${statusCounts.complete} are complete`
   153    );
   154  
   155    assert.equal(
   156      legend.querySelector('li.failed .value').textContent,
   157      statusCounts.failed,
   158      `${statusCounts.failed} are failed`
   159    );
   160  
   161    assert.equal(
   162      legend.querySelector('li.lost .value').textContent,
   163      statusCounts.lost,
   164      `${statusCounts.lost} are lost`
   165    );
   166  });
   167  
   168  test('there is no active deployment section when the job has no active deployment', function(
   169    assert
   170  ) {
   171    // TODO: it would be better to not visit two different job pages in one test, but this
   172    // way is much more convenient.
   173    job = server.create('job', { noActiveDeployment: true, type: 'service' });
   174    visit(`/jobs/${job.id}`);
   175  
   176    andThen(() => {
   177      assert.ok(findAll('.active-deployment').length === 0, 'No active deployment');
   178    });
   179  });
   180  
   181  test('the active deployment section shows up for the currently running deployment', function(
   182    assert
   183  ) {
   184    job = server.create('job', { activeDeployment: true, type: 'service' });
   185    const deployment = server.db.deployments.where({ jobId: job.id })[0];
   186    const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({
   187      deploymentId: deployment.id,
   188    });
   189    const version = server.db.jobVersions.findBy({
   190      jobId: job.id,
   191      version: deployment.versionNumber,
   192    });
   193    visit(`/jobs/${job.id}`);
   194  
   195    andThen(() => {
   196      assert.ok(findAll('.active-deployment').length === 1, 'Active deployment');
   197      assert.equal(
   198        $('.active-deployment > .boxed-section-head .badge')
   199          .get(0)
   200          .textContent.trim(),
   201        deployment.id.split('-')[0],
   202        'The active deployment is the most recent running deployment'
   203      );
   204  
   205      assert.equal(
   206        $('.active-deployment > .boxed-section-head .submit-time')
   207          .get(0)
   208          .textContent.trim(),
   209        moment(version.submitTime / 1000000).fromNow(),
   210        'Time since the job was submitted is in the active deployment header'
   211      );
   212  
   213      assert.equal(
   214        $('.deployment-metrics .label:contains("Canaries") + .value')
   215          .get(0)
   216          .textContent.trim(),
   217        `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum(
   218          taskGroupSummaries,
   219          'desiredCanaries'
   220        )}`,
   221        'Canaries, both places and desired, are in the metrics'
   222      );
   223  
   224      assert.equal(
   225        $('.deployment-metrics .label:contains("Placed") + .value')
   226          .get(0)
   227          .textContent.trim(),
   228        sum(taskGroupSummaries, 'placedAllocs'),
   229        'Placed allocs aggregates across task groups'
   230      );
   231  
   232      assert.equal(
   233        $('.deployment-metrics .label:contains("Desired") + .value')
   234          .get(0)
   235          .textContent.trim(),
   236        sum(taskGroupSummaries, 'desiredTotal'),
   237        'Desired allocs aggregates across task groups'
   238      );
   239  
   240      assert.equal(
   241        $('.deployment-metrics .label:contains("Healthy") + .value')
   242          .get(0)
   243          .textContent.trim(),
   244        sum(taskGroupSummaries, 'healthyAllocs'),
   245        'Healthy allocs aggregates across task groups'
   246      );
   247  
   248      assert.equal(
   249        $('.deployment-metrics .label:contains("Unhealthy") + .value')
   250          .get(0)
   251          .textContent.trim(),
   252        sum(taskGroupSummaries, 'unhealthyAllocs'),
   253        'Unhealthy allocs aggregates across task groups'
   254      );
   255  
   256      assert.equal(
   257        $('.deployment-metrics .notification')
   258          .get(0)
   259          .textContent.trim(),
   260        deployment.statusDescription,
   261        'Status description is in the metrics block'
   262      );
   263    });
   264  });
   265  
   266  test('the active deployment section can be expanded to show task groups and allocations', function(
   267    assert
   268  ) {
   269    job = server.create('job', { activeDeployment: true, type: 'service' });
   270    visit(`/jobs/${job.id}`);
   271  
   272    andThen(() => {
   273      assert.ok(
   274        $('.active-deployment .boxed-section-head:contains("Task Groups")').length === 0,
   275        'Task groups not found'
   276      );
   277      assert.ok(
   278        $('.active-deployment .boxed-section-head:contains("Allocations")').length === 0,
   279        'Allocations not found'
   280      );
   281    });
   282  
   283    andThen(() => {
   284      click('.active-deployment-details-toggle');
   285    });
   286  
   287    andThen(() => {
   288      assert.ok(
   289        $('.active-deployment .boxed-section-head:contains("Task Groups")').length === 1,
   290        'Task groups found'
   291      );
   292      assert.ok(
   293        $('.active-deployment .boxed-section-head:contains("Allocations")').length === 1,
   294        'Allocations found'
   295      );
   296    });
   297  });
   298  
   299  test('the evaluations table lists evaluations sorted by modify index', function(assert) {
   300    job = server.create('job');
   301    const evaluations = server.db.evaluations
   302      .where({ jobId: job.id })
   303      .sortBy('modifyIndex')
   304      .reverse();
   305  
   306    visit(`/jobs/${job.id}`);
   307  
   308    andThen(() => {
   309      assert.equal(
   310        findAll('.evaluations tbody tr').length,
   311        evaluations.length,
   312        'A row for each evaluation'
   313      );
   314  
   315      evaluations.forEach((evaluation, index) => {
   316        const row = $(findAll('.evaluations tbody tr')[index]);
   317        assert.equal(
   318          row.find('td:eq(0)').text(),
   319          evaluation.id.split('-')[0],
   320          `Short ID, row ${index}`
   321        );
   322      });
   323  
   324      const firstEvaluation = evaluations[0];
   325      const row = $(findAll('.evaluations tbody tr')[0]);
   326      assert.equal(row.find('td:eq(1)').text(), '' + firstEvaluation.priority, 'Priority');
   327      assert.equal(row.find('td:eq(2)').text(), firstEvaluation.triggeredBy, 'Triggered By');
   328      assert.equal(row.find('td:eq(3)').text(), firstEvaluation.status, 'Status');
   329    });
   330  });
   331  
   332  test('when the job has placement failures, they are called out', function(assert) {
   333    job = server.create('job', { failedPlacements: true });
   334    const failedEvaluation = server.db.evaluations
   335      .where({ jobId: job.id })
   336      .filter(evaluation => evaluation.failedTGAllocs)
   337      .sortBy('modifyIndex')
   338      .reverse()[0];
   339  
   340    const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs);
   341  
   342    visit(`/jobs/${job.id}`);
   343  
   344    andThen(() => {
   345      assert.ok(find('.placement-failures'), 'Placement failures section found');
   346  
   347      const taskGroupLabels = findAll('.placement-failures h3.title').map(title =>
   348        title.textContent.trim()
   349      );
   350      failedTaskGroupNames.forEach(name => {
   351        assert.ok(
   352          taskGroupLabels.find(label => label.includes(name)),
   353          `${name} included in placement failures list`
   354        );
   355        assert.ok(
   356          taskGroupLabels.find(label =>
   357            label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1)
   358          ),
   359          'The number of unplaced allocs = CoalescedFailures + 1'
   360        );
   361      });
   362    });
   363  });
   364  
   365  test('when the job has no placement failures, the placement failures section is gone', function(
   366    assert
   367  ) {
   368    job = server.create('job', { noFailedPlacements: true });
   369    visit(`/jobs/${job.id}`);
   370  
   371    andThen(() => {
   372      assert.notOk(find('.placement-failures'), 'Placement failures section not found');
   373    });
   374  });
   375  
   376  test('when the job is not found, an error message is shown, but the URL persists', function(
   377    assert
   378  ) {
   379    visit('/jobs/not-a-real-job');
   380  
   381    andThen(() => {
   382      assert.equal(
   383        server.pretender.handledRequests.findBy('status', 404).url,
   384        '/v1/job/not-a-real-job',
   385        'A request to the non-existent job is made'
   386      );
   387      assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
   388      assert.ok(find('.error-message'), 'Error message is shown');
   389      assert.equal(
   390        find('.error-message .title').textContent,
   391        'Not Found',
   392        'Error message is for 404'
   393      );
   394    });
   395  });
   396  
   397  moduleForAcceptance('Acceptance | job detail (with namespaces)', {
   398    beforeEach() {
   399      server.createList('namespace', 2);
   400      server.create('node');
   401      job = server.create('job', { namespaceId: server.db.namespaces[1].name });
   402      server.createList('job', 3, { namespaceId: server.db.namespaces[0].name });
   403    },
   404  });
   405  
   406  test('when there are namespaces, the job detail page states the namespace for the job', function(
   407    assert
   408  ) {
   409    const namespace = server.db.namespaces.find(job.namespaceId);
   410    visit(`/jobs/${job.id}?namespace=${namespace.name}`);
   411  
   412    andThen(() => {
   413      assert.ok(
   414        findAll('.job-stats span')[2].textContent.includes(namespace.name),
   415        'Namespace included in stats'
   416      );
   417    });
   418  });
   419  
   420  test('when switching namespaces, the app redirects to /jobs with the new namespace', function(
   421    assert
   422  ) {
   423    const namespace = server.db.namespaces.find(job.namespaceId);
   424    const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name;
   425    const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace;
   426  
   427    visit(`/jobs/${job.id}?namespace=${namespace.name}`);
   428  
   429    andThen(() => {
   430      selectChoose('.namespace-switcher', label);
   431    });
   432  
   433    andThen(() => {
   434      assert.equal(currentURL().split('?')[0], '/jobs', 'Navigated to /jobs');
   435      const jobs = server.db.jobs
   436        .where({ namespace: otherNamespace })
   437        .sortBy('modifyIndex')
   438        .reverse();
   439      assert.equal(findAll('.job-row').length, jobs.length, 'Shows the right number of jobs');
   440      jobs.forEach((job, index) => {
   441        assert.equal(
   442          $(findAll('.job-row')[index])
   443            .find('td:eq(0)')
   444            .text()
   445            .trim(),
   446          job.name,
   447          `Job ${index} is right`
   448        );
   449      });
   450    });
   451  });