278 lines
8.3 KiB
JavaScript
278 lines
8.3 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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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);
|
|
});
|
|
});
|
|
});
|