github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/line-chart-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import {
     7    find,
     8    findAll,
     9    click,
    10    render,
    11    triggerEvent,
    12  } from '@ember/test-helpers';
    13  import { module, test } from 'qunit';
    14  import { setupRenderingTest } from 'ember-qunit';
    15  import hbs from 'htmlbars-inline-precompile';
    16  import sinon from 'sinon';
    17  import moment from 'moment';
    18  import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
    19  
    20  const REF_DATE = new Date();
    21  
    22  module('Integration | Component | line-chart', function (hooks) {
    23    setupRenderingTest(hooks);
    24  
    25    test('when a chart has annotations, they are rendered in order', async function (assert) {
    26      assert.expect(4);
    27  
    28      const annotations = [
    29        { x: 2, type: 'info' },
    30        { x: 1, type: 'error' },
    31        { x: 3, type: 'info' },
    32      ];
    33      this.setProperties({
    34        annotations,
    35        data: [
    36          { x: 1, y: 1 },
    37          { x: 10, y: 10 },
    38        ],
    39      });
    40  
    41      await render(hbs`
    42        <LineChart
    43          @xProp="x"
    44          @yProp="y"
    45          @data={{this.data}}>
    46          <:after as |c|>
    47            <c.VAnnotations @annotations={{this.annotations}} />
    48          </:after>
    49        </LineChart>
    50      `);
    51  
    52      const sortedAnnotations = annotations.sortBy('x');
    53      findAll('[data-test-annotation]').forEach((annotation, idx) => {
    54        const datum = sortedAnnotations[idx];
    55        assert.equal(
    56          annotation.querySelector('button').getAttribute('title'),
    57          `${datum.type} event at ${datum.x}`
    58        );
    59      });
    60  
    61      await componentA11yAudit(this.element, assert);
    62    });
    63  
    64    test('when a chart has annotations and is timeseries, annotations are sorted reverse-chronologically', async function (assert) {
    65      assert.expect(3);
    66  
    67      const annotations = [
    68        {
    69          x: moment(REF_DATE).add(2, 'd').toDate(),
    70          type: 'info',
    71        },
    72        {
    73          x: moment(REF_DATE).add(1, 'd').toDate(),
    74          type: 'error',
    75        },
    76        {
    77          x: moment(REF_DATE).add(3, 'd').toDate(),
    78          type: 'info',
    79        },
    80      ];
    81      this.setProperties({
    82        annotations,
    83        data: [
    84          { x: 1, y: 1 },
    85          { x: 10, y: 10 },
    86        ],
    87      });
    88  
    89      await render(hbs`
    90        <LineChart
    91          @xProp="x"
    92          @yProp="y"
    93          @timeseries={{true}}
    94          @data={{this.data}}>
    95          <:after as |c|>
    96            <c.VAnnotations @annotations={{this.annotations}} />
    97          </:after>
    98        </LineChart>
    99      `);
   100  
   101      const sortedAnnotations = annotations.sortBy('x').reverse();
   102      findAll('[data-test-annotation]').forEach((annotation, idx) => {
   103        const datum = sortedAnnotations[idx];
   104        assert.equal(
   105          annotation.querySelector('button').getAttribute('title'),
   106          `${datum.type} event at ${moment(datum.x).format('MMM DD, HH:mm')}`
   107        );
   108      });
   109    });
   110  
   111    test('clicking annotations calls the onAnnotationClick action with the annotation as an argument', async function (assert) {
   112      const annotations = [{ x: 2, type: 'info', meta: { data: 'here' } }];
   113      this.setProperties({
   114        annotations,
   115        data: [
   116          { x: 1, y: 1 },
   117          { x: 10, y: 10 },
   118        ],
   119        click: sinon.spy(),
   120      });
   121  
   122      await render(hbs`
   123        <LineChart
   124          @xProp="x"
   125          @yProp="y"
   126          @data={{this.data}}>
   127          <:after as |c|>
   128            <c.VAnnotations @annotations={{this.annotations}} @annotationClick={{this.click}} />
   129          </:after>
   130        </LineChart>
   131      `);
   132  
   133      await click('[data-test-annotation] button');
   134      assert.ok(this.click.calledWith(annotations[0]));
   135    });
   136  
   137    test('annotations will have staggered heights when too close to be positioned side-by-side', async function (assert) {
   138      assert.expect(4);
   139  
   140      const annotations = [
   141        { x: 2, type: 'info' },
   142        { x: 2.4, type: 'error' },
   143        { x: 9, type: 'info' },
   144      ];
   145      this.setProperties({
   146        annotations,
   147        data: [
   148          { x: 1, y: 1 },
   149          { x: 10, y: 10 },
   150        ],
   151        click: sinon.spy(),
   152      });
   153  
   154      await render(hbs`
   155        <div style="width:200px;">
   156          <LineChart
   157            @xProp="x"
   158            @yProp="y"
   159            @data={{this.data}}>
   160            <:after as |c|>
   161              <c.VAnnotations @annotations={{this.annotations}} />
   162            </:after>
   163          </LineChart>
   164        </div>
   165      `);
   166  
   167      const annotationEls = findAll('[data-test-annotation]');
   168      assert.notOk(annotationEls[0].classList.contains('is-staggered'));
   169      assert.ok(annotationEls[1].classList.contains('is-staggered'));
   170      assert.notOk(annotationEls[2].classList.contains('is-staggered'));
   171  
   172      await componentA11yAudit(this.element, assert);
   173    });
   174  
   175    test('horizontal annotations render in order', async function (assert) {
   176      assert.expect(3);
   177  
   178      const annotations = [
   179        { y: 2, label: 'label one' },
   180        { y: 9, label: 'label three' },
   181        { y: 2.4, label: 'label two' },
   182      ];
   183      this.setProperties({
   184        annotations,
   185        data: [
   186          { x: 1, y: 1 },
   187          { x: 10, y: 10 },
   188        ],
   189      });
   190  
   191      await render(hbs`
   192        <LineChart
   193          @xProp="x"
   194          @yProp="y"
   195          @data={{this.data}}>
   196          <:after as |c|>
   197            <c.HAnnotations @annotations={{this.annotations}} @labelProp="label" />
   198          </:after>
   199        </LineChart>
   200      `);
   201  
   202      const annotationEls = findAll('[data-test-annotation]');
   203      annotations
   204        .sortBy('y')
   205        .reverse()
   206        .forEach((annotation, index) => {
   207          assert.equal(annotationEls[index].textContent.trim(), annotation.label);
   208        });
   209    });
   210  
   211    test('the tooltip includes information on the data closest to the mouse', async function (assert) {
   212      assert.expect(8);
   213  
   214      const series1 = [
   215        { x: 1, y: 2 },
   216        { x: 3, y: 3 },
   217        { x: 5, y: 4 },
   218      ];
   219      const series2 = [
   220        { x: 2, y: 10 },
   221        { x: 4, y: 9 },
   222        { x: 6, y: 8 },
   223      ];
   224      this.setProperties({
   225        data: [
   226          { series: 'One', data: series1 },
   227          { series: 'Two', data: series2 },
   228        ],
   229      });
   230  
   231      await render(hbs`
   232        <div style="width:500px;margin-top:100px">
   233          <LineChart
   234            @xProp="x"
   235            @yProp="y"
   236            @dataProp="data"
   237            @data={{this.data}}>
   238            <:svg as |c|>
   239              {{#each this.data as |series idx|}}
   240                <c.Area @data={{series.data}} @colorScale="blues" @index={{idx}} />
   241              {{/each}}
   242            </:svg>
   243            <:after as |c|>
   244              <c.Tooltip as |series datum index|>
   245                <li>
   246                  <span class="label"><span class="color-swatch swatch-blues swatch-blues-{{index}}" />{{series.series}}</span>
   247                  <span class="value">{{datum.formattedY}}</span>
   248                </li>
   249              </c.Tooltip>
   250            </:after>
   251          </LineChart>
   252        </div>
   253      `);
   254  
   255      // All tooltip events are attached to the hover target
   256      const hoverTarget = find('[data-test-hover-target]');
   257  
   258      // Mouse to data mapping happens based on the clientX of the MouseEvent
   259      const bbox = hoverTarget.getBoundingClientRect();
   260      // The MouseEvent needs to be translated based on the location of the hover target
   261      const xOffset = bbox.x;
   262      // An interval here is the width between x values given the fixed dimensions of the line chart
   263      // and the domain of the data
   264      const interval = bbox.width / 5;
   265  
   266      // MouseEnter triggers the tooltip visibility
   267      await triggerEvent(hoverTarget, 'mouseenter');
   268      // MouseMove positions the tooltip and updates the active datum
   269      await triggerEvent(hoverTarget, 'mousemove', {
   270        clientX: xOffset + interval * 1 + 5,
   271      });
   272      assert.equal(findAll('[data-test-chart-tooltip] li').length, 1);
   273      assert.equal(
   274        find('[data-test-chart-tooltip] .label').textContent.trim(),
   275        this.data[1].series
   276      );
   277      assert.equal(
   278        find('[data-test-chart-tooltip] .value').textContent.trim(),
   279        series2.find((d) => d.x === 2).y
   280      );
   281  
   282      // When the mouse falls between points and each series has points with different x values,
   283      // points will only be shown in the tooltip if they are close enough to the closest point
   284      // to the cursor.
   285      // This event is intentionally between points such that both points are within proximity.
   286      const expected = [
   287        { label: this.data[0].series, value: series1.find((d) => d.x === 3).y },
   288        { label: this.data[1].series, value: series2.find((d) => d.x === 2).y },
   289      ];
   290      await triggerEvent(hoverTarget, 'mousemove', {
   291        clientX: xOffset + interval * 1.5 + 5,
   292      });
   293      assert.equal(findAll('[data-test-chart-tooltip] li').length, 2);
   294      findAll('[data-test-chart-tooltip] li').forEach((tooltipEntry, index) => {
   295        assert.equal(
   296          tooltipEntry.querySelector('.label').textContent.trim(),
   297          expected[index].label
   298        );
   299        assert.equal(
   300          tooltipEntry.querySelector('.value').textContent.trim(),
   301          expected[index].value
   302        );
   303      });
   304    });
   305  });