open-nomad/ui/tests/integration/components/line-chart-test.js
Jai Bhagat 3350f3fb11 ui: apply new qunit linting rules to tests
Async tests should use  in integrations tests.
Acceptance tests are using Mirage and can't use
since we can't know the number of assertions.
2022-01-20 10:01:35 -05:00

301 lines
8.5 KiB
JavaScript

import {
find,
findAll,
click,
render,
triggerEvent,
} from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import moment from 'moment';
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
const REF_DATE = new Date();
module('Integration | Component | line-chart', function (hooks) {
setupRenderingTest(hooks);
test('when a chart has annotations, they are rendered in order', async function (assert) {
assert.expect(4);
const annotations = [
{ x: 2, type: 'info' },
{ x: 1, type: 'error' },
{ x: 3, type: 'info' },
];
this.setProperties({
annotations,
data: [
{ x: 1, y: 1 },
{ x: 10, y: 10 },
],
});
await render(hbs`
<LineChart
@xProp="x"
@yProp="y"
@data={{this.data}}>
<:after as |c|>
<c.VAnnotations @annotations={{this.annotations}} />
</:after>
</LineChart>
`);
const sortedAnnotations = annotations.sortBy('x');
findAll('[data-test-annotation]').forEach((annotation, idx) => {
const datum = sortedAnnotations[idx];
assert.equal(
annotation.querySelector('button').getAttribute('title'),
`${datum.type} event at ${datum.x}`
);
});
await componentA11yAudit(this.element, assert);
});
test('when a chart has annotations and is timeseries, annotations are sorted reverse-chronologically', async function (assert) {
assert.expect(3);
const annotations = [
{
x: moment(REF_DATE).add(2, 'd').toDate(),
type: 'info',
},
{
x: moment(REF_DATE).add(1, 'd').toDate(),
type: 'error',
},
{
x: moment(REF_DATE).add(3, 'd').toDate(),
type: 'info',
},
];
this.setProperties({
annotations,
data: [
{ x: 1, y: 1 },
{ x: 10, y: 10 },
],
});
await render(hbs`
<LineChart
@xProp="x"
@yProp="y"
@timeseries={{true}}
@data={{this.data}}>
<:after as |c|>
<c.VAnnotations @annotations={{this.annotations}} />
</:after>
</LineChart>
`);
const sortedAnnotations = annotations.sortBy('x').reverse();
findAll('[data-test-annotation]').forEach((annotation, idx) => {
const datum = sortedAnnotations[idx];
assert.equal(
annotation.querySelector('button').getAttribute('title'),
`${datum.type} event at ${moment(datum.x).format('MMM DD, HH:mm')}`
);
});
});
test('clicking annotations calls the onAnnotationClick action with the annotation as an argument', async function (assert) {
const annotations = [{ x: 2, type: 'info', meta: { data: 'here' } }];
this.setProperties({
annotations,
data: [
{ x: 1, y: 1 },
{ x: 10, y: 10 },
],
click: sinon.spy(),
});
await render(hbs`
<LineChart
@xProp="x"
@yProp="y"
@data={{this.data}}>
<:after as |c|>
<c.VAnnotations @annotations={{this.annotations}} @annotationClick={{this.click}} />
</:after>
</LineChart>
`);
await click('[data-test-annotation] button');
assert.ok(this.click.calledWith(annotations[0]));
});
test('annotations will have staggered heights when too close to be positioned side-by-side', async function (assert) {
assert.expect(4);
const annotations = [
{ x: 2, type: 'info' },
{ x: 2.4, type: 'error' },
{ x: 9, type: 'info' },
];
this.setProperties({
annotations,
data: [
{ x: 1, y: 1 },
{ x: 10, y: 10 },
],
click: sinon.spy(),
});
await render(hbs`
<div style="width:200px;">
<LineChart
@xProp="x"
@yProp="y"
@data={{this.data}}>
<:after as |c|>
<c.VAnnotations @annotations={{this.annotations}} />
</:after>
</LineChart>
</div>
`);
const annotationEls = findAll('[data-test-annotation]');
assert.notOk(annotationEls[0].classList.contains('is-staggered'));
assert.ok(annotationEls[1].classList.contains('is-staggered'));
assert.notOk(annotationEls[2].classList.contains('is-staggered'));
await componentA11yAudit(this.element, assert);
});
test('horizontal annotations render in order', async function (assert) {
assert.expect(3);
const annotations = [
{ y: 2, label: 'label one' },
{ y: 9, label: 'label three' },
{ y: 2.4, label: 'label two' },
];
this.setProperties({
annotations,
data: [
{ x: 1, y: 1 },
{ x: 10, y: 10 },
],
});
await render(hbs`
<LineChart
@xProp="x"
@yProp="y"
@data={{this.data}}>
<:after as |c|>
<c.HAnnotations @annotations={{this.annotations}} @labelProp="label" />
</:after>
</LineChart>
`);
const annotationEls = findAll('[data-test-annotation]');
annotations
.sortBy('y')
.reverse()
.forEach((annotation, index) => {
assert.equal(annotationEls[index].textContent.trim(), annotation.label);
});
});
test('the tooltip includes information on the data closest to the mouse', async function (assert) {
assert.expect(8);
const series1 = [
{ x: 1, y: 2 },
{ x: 3, y: 3 },
{ x: 5, y: 4 },
];
const series2 = [
{ x: 2, y: 10 },
{ x: 4, y: 9 },
{ x: 6, y: 8 },
];
this.setProperties({
data: [
{ series: 'One', data: series1 },
{ series: 'Two', data: series2 },
],
});
await render(hbs`
<div style="width:500px;margin-top:100px">
<LineChart
@xProp="x"
@yProp="y"
@dataProp="data"
@data={{this.data}}>
<:svg as |c|>
{{#each this.data as |series idx|}}
<c.Area @data={{series.data}} @colorScale="blues" @index={{idx}} />
{{/each}}
</:svg>
<:after as |c|>
<c.Tooltip as |series datum index|>
<li>
<span class="label"><span class="color-swatch swatch-blues swatch-blues-{{index}}" />{{series.series}}</span>
<span class="value">{{datum.formattedY}}</span>
</li>
</c.Tooltip>
</:after>
</LineChart>
</div>
`);
// All tooltip events are attached to the hover target
const hoverTarget = find('[data-test-hover-target]');
// Mouse to data mapping happens based on the clientX of the MouseEvent
const bbox = hoverTarget.getBoundingClientRect();
// The MouseEvent needs to be translated based on the location of the hover target
const xOffset = bbox.x;
// An interval here is the width between x values given the fixed dimensions of the line chart
// and the domain of the data
const interval = bbox.width / 5;
// MouseEnter triggers the tooltip visibility
await triggerEvent(hoverTarget, 'mouseenter');
// MouseMove positions the tooltip and updates the active datum
await triggerEvent(hoverTarget, 'mousemove', {
clientX: xOffset + interval * 1 + 5,
});
assert.equal(findAll('[data-test-chart-tooltip] li').length, 1);
assert.equal(
find('[data-test-chart-tooltip] .label').textContent.trim(),
this.data[1].series
);
assert.equal(
find('[data-test-chart-tooltip] .value').textContent.trim(),
series2.find((d) => d.x === 2).y
);
// When the mouse falls between points and each series has points with different x values,
// points will only be shown in the tooltip if they are close enough to the closest point
// to the cursor.
// This event is intentionally between points such that both points are within proximity.
const expected = [
{ label: this.data[0].series, value: series1.find((d) => d.x === 3).y },
{ label: this.data[1].series, value: series2.find((d) => d.x === 2).y },
];
await triggerEvent(hoverTarget, 'mousemove', {
clientX: xOffset + interval * 1.5 + 5,
});
assert.equal(findAll('[data-test-chart-tooltip] li').length, 2);
findAll('[data-test-chart-tooltip] li').forEach((tooltipEntry, index) => {
assert.equal(
tooltipEntry.querySelector('.label').textContent.trim(),
expected[index].label
);
assert.equal(
tooltipEntry.querySelector('.value').textContent.trim(),
expected[index].value
);
});
});
});