UI/1.11 client count component tests (#15748)

* add line chart test

* add empty state option to line chart

* add empty state test

* add tooltip coverage

* add test files

* add monthly usage tests

* finish tests

* tidying

* address comments, add average test

* finish tests broken from calendar
This commit is contained in:
claire bontempo 2022-06-03 15:47:19 -07:00 committed by GitHub
parent 7f7ef1cfe9
commit 55dba55bbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 3518 additions and 103 deletions

View File

@ -8,6 +8,7 @@ import { axisLeft } from 'd3-axis';
import { max, maxIndex } from 'd3-array';
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from 'vault/utils/chart-helpers';
import { tracked } from '@glimmer/tracking';
import { formatNumber } from 'core/helpers/format-number';
/**
* @module HorizontalBarChart
@ -22,6 +23,7 @@ import { tracked } from '@glimmer/tracking';
* @param {string} labelKey - string of key name for label value in chart data
* @param {string} xKey - string of key name for x value in chart data
* @param {object} totalCounts - object to calculate percentage for tooltip
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
*/
// SIZING CONSTANTS
@ -247,7 +249,7 @@ export default class HorizontalBarChart extends Component {
.data(dataset)
.enter()
.append('text')
.text((d) => d[xKey])
.text((d) => formatNumber([d[xKey]]))
.attr('fill', '#000')
.attr('class', 'total-value')
.style('font-size', '.8rem')

View File

@ -14,6 +14,7 @@ import {
formatNumbers,
} from 'vault/utils/chart-helpers';
import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
import { formatNumber } from 'core/helpers/format-number';
/**
* @module LineChart
@ -21,10 +22,12 @@ import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
*
* @example
* ```js
* <LineChart @dataset={dataset} />
* <LineChart @dataset={{dataset}} @upgradeData={{this.versionHistory}}/>
* ```
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
* @param {array} upgradeData - array of objects containing version history from the /version-history endpoint
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
*/
export default class LineChart extends Component {
@ -42,6 +45,27 @@ export default class LineChart extends Component {
return this.args.xKey || 'month';
}
get upgradeData() {
const upgradeData = this.args.upgradeData;
if (!upgradeData) return null;
if (!Array.isArray(upgradeData)) {
console.debug('upgradeData must be an array of objects containing upgrade history');
return null;
} else if (!Object.keys(upgradeData[0]).includes('timestampInstalled')) {
console.debug(
`upgrade must be an object with the following key names: ['id', 'previousVersion', 'timestampInstalled']`
);
return null;
} else {
return upgradeData?.map((versionData) => {
return {
[this.xKey]: parseAPITimestamp(versionData.timestampInstalled, 'M/yy'),
...versionData,
};
});
}
}
@action removeTooltip() {
this.tooltipTarget = null;
}
@ -49,17 +73,10 @@ export default class LineChart extends Component {
@action
renderChart(element, [chartData]) {
const dataset = chartData;
const upgradeData = [];
if (this.args.upgradeData) {
this.args.upgradeData.forEach((versionData) =>
upgradeData.push({ month: parseAPITimestamp(versionData.timestampInstalled, 'M/yy'), ...versionData })
);
}
const filteredData = dataset.filter((e) => Object.keys(e).includes(this.yKey)); // months with data will contain a 'clients' key (otherwise only a timestamp)
const domainMax = max(filteredData.map((d) => d[this.yKey]));
const chartSvg = select(element);
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
// clear out DOM before appending anything
chartSvg.selectAll('g').remove().exit().data(filteredData).enter();
@ -93,7 +110,9 @@ export default class LineChart extends Component {
chartSvg.selectAll('.domain').remove();
const findUpgradeData = (datum) => {
return upgradeData.find((upgrade) => upgrade[this.xKey] === datum[this.xKey]);
return this.upgradeData
? this.upgradeData.find((upgrade) => upgrade[this.xKey] === datum[this.xKey])
: null;
};
// VERSION UPGRADE INDICATOR
@ -104,6 +123,7 @@ export default class LineChart extends Component {
.enter()
.append('circle')
.attr('class', 'upgrade-circle')
.attr('data-test-line-chart', (d) => `upgrade-${d[this.xKey]}`)
.attr('fill', UPGRADE_WARNING)
.style('opacity', (d) => (findUpgradeData(d) ? '1' : '0'))
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
@ -158,8 +178,8 @@ export default class LineChart extends Component {
hoverCircles.on('mouseover', (data) => {
// TODO: how to generalize this?
this.tooltipMonth = formatChartDate(data[this.xKey]);
this.tooltipTotal = data[this.yKey] + ' total clients';
this.tooltipNew = (data?.new_clients[this.yKey] || '0') + ' new clients';
this.tooltipTotal = formatNumber([data[this.yKey]]) + ' total clients';
this.tooltipNew = (formatNumber([data?.new_clients[this.yKey]]) || '0') + ' new clients';
this.tooltipUpgradeText = '';
let upgradeInfo = findUpgradeData(data);
if (upgradeInfo) {

View File

@ -1,5 +1,5 @@
import Component from '@glimmer/component';
import { calculateAverageClients } from 'vault/utils/chart-helpers';
import { calculateAverage } from 'vault/utils/chart-helpers';
/**
* @module MonthlyUsage
@ -34,13 +34,13 @@ import { calculateAverageClients } from 'vault/utils/chart-helpers';
*/
export default class MonthlyUsage extends Component {
get averageTotalClients() {
return calculateAverageClients(this.args.verticalBarChartData, 'clients') || '0';
return calculateAverage(this.args.verticalBarChartData, 'clients') || '0';
}
get averageNewClients() {
return (
calculateAverageClients(
this.args.verticalBarChartData.map((d) => d.new_clients),
calculateAverage(
this.args.verticalBarChartData?.map((d) => d.new_clients),
'clients'
) || '0'
);

View File

@ -1,5 +1,5 @@
import Component from '@glimmer/component';
import { calculateAverageClients } from 'vault/utils/chart-helpers';
import { calculateAverage } from 'vault/utils/chart-helpers';
/**
* @module RunningTotal
@ -45,14 +45,14 @@ export default class RunningTotal extends Component {
get entityClientData() {
return {
runningTotal: this.args.runningTotals.entity_clients,
averageNewClients: calculateAverageClients(this.args.barChartData, 'entity_clients') || '0',
averageNewClients: calculateAverage(this.args.barChartData, 'entity_clients') || '0',
};
}
get nonEntityClientData() {
return {
runningTotal: this.args.runningTotals.non_entity_clients,
averageNewClients: calculateAverageClients(this.args.barChartData, 'non_entity_clients') || '0',
averageNewClients: calculateAverage(this.args.barChartData, 'non_entity_clients') || '0',
};
}
@ -71,8 +71,8 @@ export default class RunningTotal extends Component {
}
get showSingleMonth() {
if (this.args.barChartData.length === 1) {
const monthData = this.args.lineChartData[0];
if (this.args.lineChartData?.length === 1) {
const monthData = this.args?.lineChartData[0];
return {
total: {
total: monthData.clients,

View File

@ -14,6 +14,7 @@ import {
TRANSLATE,
formatNumbers,
} from 'vault/utils/chart-helpers';
import { formatNumber } from 'core/helpers/format-number';
/**
* @module VerticalBarChart
@ -27,6 +28,7 @@ import {
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
*/
export default class VerticalBarChart extends Component {
@ -83,6 +85,7 @@ export default class VerticalBarChart extends Component {
.append('rect')
.attr('width', '7px')
.attr('class', 'data-bar')
.attr('data-test-vertical-chart', 'data-bar')
.attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`)
.attr('x', ({ data }) => xScale(data[this.xKey])) // uses destructuring because was data.data.month
.attr('y', (data) => `${100 - yScale(data[1])}%`); // subtract higher than 100% to give space for x axis ticks
@ -101,8 +104,13 @@ export default class VerticalBarChart extends Component {
const xAxis = axisBottom(xScale).tickSize(0);
yAxis(chartSvg.append('g'));
xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`));
yAxis(chartSvg.append('g').attr('data-test-vertical-chart', 'y-axis-labels'));
xAxis(
chartSvg
.append('g')
.attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)
.attr('data-test-vertical-chart', 'x-axis-labels')
);
chartSvg.selectAll('.domain').remove(); // remove domain lines
@ -129,9 +137,9 @@ export default class VerticalBarChart extends Component {
// MOUSE EVENT FOR TOOLTIP
tooltipRect.on('mouseover', (data) => {
let hoveredMonth = data[this.xKey];
this.tooltipTotal = `${data[this.yKey]} ${data.new_clients ? 'total' : 'new'} clients`;
this.entityClients = `${data.entity_clients} entity clients`;
this.nonEntityClients = `${data.non_entity_clients} non-entity clients`;
this.tooltipTotal = `${formatNumber([data[this.yKey]])} ${data.new_clients ? 'total' : 'new'} clients`;
this.entityClients = `${formatNumber([data.entity_clients])} entity clients`;
this.nonEntityClients = `${formatNumber([data.non_entity_clients])} non-entity clients`;
let node = chartSvg
.selectAll('rect.data-bar')
// filter for the top data bar (so y-coord !== 0) with matching month

View File

@ -130,10 +130,17 @@
max-width: none;
padding-right: 20px;
padding-left: 20px;
display: flex;
> div {
box-shadow: none !important;
}
> div.empty-state {
white-space: nowrap;
align-self: stretch;
width: 100%;
}
}
.chart-subTitle {

View File

@ -24,7 +24,10 @@
{{#if this.barChartTotalClients}}
{{#if (or @isDateRange @isCurrentMonth)}}
<div class="chart-container-wide" data-test-chart-container="total-clients">
<div
class={{concat (unless this.barChartTotalClients "chart-empty-state ") "chart-container-wide"}}
data-test-chart-container="total-clients"
>
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}

View File

@ -204,6 +204,7 @@
<button
type="button"
class="button link"
data-test-date-modal-month={{month}}
disabled={{if (lt index this.allowedMonthMax) false true}}
{{on "click" (fn this.selectStartMonth month D.actions)}}
>
@ -230,6 +231,7 @@
<button
type="button"
class="button link"
data-test-date-modal-year={{year}}
disabled={{if (eq year this.disabledYear) true false}}
{{on "click" (fn this.selectStartYear year D.actions)}}
>

View File

@ -8,9 +8,7 @@
>
</svg>
{{else}}
<div class="chart-empty-state">
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
</div>
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
{{/if}}
{{#if this.tooltipTarget}}
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}

View File

@ -1,11 +1,15 @@
<svg
data-test-line-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>
{{#if @dataset}}
<svg
data-test-line-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>
{{else}}
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
{{/if}}
{{! TOOLTIP }}

Before

Width:  |  Height:  |  Size: 988 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -6,7 +6,7 @@
</p>
</div>
<div class="chart-container-wide">
<div class={{concat (unless @verticalBarChartData "chart-empty-state ") "chart-container-wide"}}>
<Clients::VerticalBarChart @dataset={{@verticalBarChartData}} @chartLegend={{@chartLegend}} />
</div>
@ -17,30 +17,31 @@
</p>
</div>
<div class="data-details-top">
<div class="data-details-top" data-test-monthly-usage-average-total>
<h3 class="data-details">Average total clients per month</h3>
<p class="data-details">
{{format-number this.averageTotalClients}}
</p>
</div>
<div class="data-details-bottom">
<div class="data-details-bottom" data-test-monthly-usage-average-new>
<h3 class="data-details">Average new clients per month</h3>
<p class="data-details">
{{format-number this.averageNewClients}}
</p>
</div>
<div class="timestamp">
<div data-test-monthly-usage-timestamp class="timestamp">
{{#if @timestamp}}
Updated
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
{{/if}}
</div>
<div class="legend-right">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
{{#if @verticalBarChartData}}
<div data-test-monthly-usage-legend class="legend-right">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
{{/if}}
</div>

View File

@ -7,7 +7,7 @@
The total client count number is an important consideration for Vault billing.
</p>
</div>
<div class="single-month-stats">
<div class="single-month-stats" data-test-new>
<div class="single-month-section-title">
<StatText
@label="New clients"
@ -23,7 +23,7 @@
<StatText @label="Non-entity clients" @value={{this.showSingleMonth.new.nonEntityClients}} @size="m" />
</div>
</div>
<div class="single-month-stats">
<div class="single-month-stats" data-test-total>
<div class="single-month-section-title">
<StatText
@label="Total monthly clients"
@ -51,7 +51,7 @@
</p>
</div>
<div class="chart-container-wide">
<div class={{concat (unless @lineChartData "chart-empty-state ") "chart-container-wide"}}>
<Clients::LineChart @dataset={{@lineChartData}} @upgradeData={{@upgradeData}} />
</div>
@ -109,17 +109,19 @@
</p>
</div>
<div class="timestamp">
<div class="timestamp" data-test-running-total-timestamp>
{{#if @timestamp}}
Updated
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
{{/if}}
</div>
<div class="legend-right">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
{{#if this.hasAverageNewClients}}
<div class="legend-right" data-test-running-total-legend>
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
{{/if}}
</div>
</div>
{{/if}}

View File

@ -8,9 +8,7 @@
>
</svg>
{{else}}
<div class="chart-empty-state">
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
</div>
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
{{/if}}
{{! TOOLTIP }}

View File

@ -27,9 +27,11 @@ export function formatTooltipNumber(value) {
return new Intl.NumberFormat().format(value);
}
export function calculateAverageClients(dataset, objectKey) {
// dataset is an array of objects (consumed by the chart components)
// objectKey is the key of the integer we want to calculate, ex: 'entity_clients', 'non_entity_clients', 'clients'
let getIntegers = dataset.map((d) => (d[objectKey] ? d[objectKey] : 0)); // if undefined no data, so return 0
return getIntegers.length !== 0 ? Math.round(mean(getIntegers)) : null;
export function calculateAverage(dataset, objectKey) {
if (!Array.isArray(dataset) || dataset?.length === 0) return null;
// if an array of objects, objectKey of the integer we want to calculate, ex: 'entity_clients'
// if d[objectKey] is undefined there is no value, so return 0
const getIntegers = objectKey ? dataset?.map((d) => (d[objectKey] ? d[objectKey] : 0)) : dataset;
let checkIntegers = getIntegers.every((n) => Number.isInteger(n)); // decimals will be false
return checkIntegers ? Math.round(mean(getIntegers)) : null;
}

View File

@ -1,6 +1,6 @@
<div
class={{concat "stat-text-container " @size (unless @subText "-no-subText")}}
data-test-stat-text-container
data-test-stat-text-container={{(or @label "true")}}
...attributes
>
<div class="stat-label has-bottom-margin-xs">{{@label}}</div>

View File

@ -34,7 +34,7 @@ export const parseRFC3339 = (timestamp) => {
return date ? [`${date.getFullYear()}`, date.getMonth()] : null;
};
// convert MM/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
// convert M/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
export function formatChartDate(date) {
let array = date.split('/');
array.splice(1, 0, '01');

View File

@ -1684,6 +1684,7 @@ const MOCK_MONTHLY_DATA = [
},
},
{
timestamp: formatISO(addMonths(UPGRADE_DATE, 3)),
counts: {
distinct_entities: 0,
entity_clients: 10873,
@ -2236,7 +2237,7 @@ const MOCK_MONTHLY_DATA = [
},
},
{
timestamp: formatISO(addMonths(UPGRADE_DATE, 3)),
timestamp: formatISO(addMonths(UPGRADE_DATE, 4)),
counts: {
distinct_entities: 0,
entity_clients: 10342,

View File

@ -9,6 +9,8 @@ import { SELECTORS, overrideResponse } from '../helpers/clients';
import { create } from 'ember-cli-page-object';
import ss from 'vault/tests/pages/components/search-select';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
const searchSelect = create(ss);
const NEW_DATE = new Date();
@ -133,18 +135,47 @@ module('Acceptance | clients history tab', function (hooks) {
});
test('updates correctly when querying date ranges', async function (assert) {
assert.expect(17);
assert.expect(26);
// TODO CMB: wire up dynamically generated activity to mirage clients handler
// const activity = generateActivityResponse(5, LICENSE_START, LAST_MONTH);
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history');
// change billing start month
// query for single, historical month with no new counts
await click(SELECTORS.rangeDropdown);
await click('[data-test-show-calendar]');
if (parseInt(find('[data-test-display-year]').innerText) > LICENSE_START.getFullYear()) {
await click('[data-test-previous-year]');
}
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`));
assert.dom('[data-test-usage-stats]').exists('total usage stats show');
assert
.dom(SELECTORS.runningTotalMonthStats)
.doesNotExist('running total single month stat boxes do not show');
assert
.dom(SELECTORS.runningTotalMonthlyCharts)
.doesNotExist('running total month over month charts do not show');
assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('does not show monthly usage block');
assert.dom(SELECTORS.attributionBlock).exists('attribution area shows');
assert
.dom('[data-test-chart-container="new-clients"] [data-test-component="empty-state"]')
.exists('new client attribution has empty state');
assert
.dom('[data-test-empty-state-subtext]')
.hasText('There are no new clients for this namespace during this time period. ');
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows');
// reset to billing period
await click('[data-test-popup-menu-trigger]');
await click('[data-test-current-billing-period]');
// change billing start to month/year of first upgrade
await click('[data-test-start-date-editor] button');
await click(SELECTORS.monthDropdown);
await click(find('.menu-list button:not([disabled])'));
await click(find(`[data-test-date-modal-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`));
await click(SELECTORS.yearDropdown);
await click(find('.menu-list button:not([disabled])'));
await click(find(`[data-test-date-modal-year="${UPGRADE_DATE.getFullYear()}`));
await click('[data-test-modal-save]');
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
@ -154,7 +185,7 @@ module('Acceptance | clients history tab', function (hooks) {
.exists('Shows running totals with monthly breakdown charts');
assert
.dom(find('[data-test-line-chart="x-axis-labels"] g.tick text'))
.hasText('1/22', 'x-axis labels start with updated billing start month');
.hasText(`${format(UPGRADE_DATE, 'M/yy')}`, 'x-axis labels start with updated billing start month');
assert.equal(
findAll('[data-test-line-chart="plot-point"]').length,
5,
@ -164,9 +195,10 @@ module('Acceptance | clients history tab', function (hooks) {
// query custom end month
await click(SELECTORS.rangeDropdown);
await click('[data-test-show-calendar]');
let readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly');
let clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m));
await click(clickableMonths[1]);
if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) {
await click('[data-test-future-year]');
}
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LAST_MONTH.getMonth() - 2]}]`));
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block');
@ -175,23 +207,27 @@ module('Acceptance | clients history tab', function (hooks) {
.exists('Shows running totals with monthly breakdown charts');
assert.equal(
findAll('[data-test-line-chart="plot-point"]').length,
2,
`line chart plots 2 points to match query`
3,
`line chart plots 3 points to match query`
);
let xAxisLabels = findAll('[data-test-line-chart="x-axis-labels"] g.tick text');
assert
.dom(findAll('[data-test-line-chart="x-axis-labels"] g.tick text')[1])
.hasText('2/22', 'x-axis labels start with updated billing start month');
.dom(xAxisLabels[xAxisLabels.length - 1])
.hasText(`${format(subMonths(LAST_MONTH, 2), 'M/yy')}`, 'x-axis labels end with queried end month');
// query for single, historical month
await click(SELECTORS.rangeDropdown);
await click('[data-test-show-calendar]');
readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly');
clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m));
await click(clickableMonths[0]);
if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) {
await click('[data-test-future-year]');
}
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}]`));
assert.dom(SELECTORS.runningTotalMonthStats).exists('running total single month stat boxes show');
assert
.dom(SELECTORS.runningTotalMonthlyCharts)
.doesNotExist('running total month over month charts do not show');
assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('Does not show monthly usage block');
assert.dom(SELECTORS.attributionBlock).exists('attribution area shows');
assert.dom('[data-test-chart-container="new-clients"]').exists('new client attribution chart shows');
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows');
@ -203,8 +239,7 @@ module('Acceptance | clients history tab', function (hooks) {
// query month older than count start date
await click('[data-test-start-date-editor] button');
await click(SELECTORS.yearDropdown);
let years = findAll('.menu-list button:not([disabled])');
await click(years[years.length - 1]);
await click(find(`[data-test-date-modal-year="${LICENSE_START.getFullYear() - 3}`));
await click('[data-test-modal-save]');
assert

View File

@ -8,20 +8,14 @@ import { Response } from 'miragejs';
Filtering (data with mounts)
Filtering (data without mounts)
Filtering (data without mounts)
* -- HISTORY ONLY --
* -- HISTORY ONLY --
Filtering different date ranges (hist only)
Upgrade warning
No permissions for license
Version
queries available
queries unavailable
License start date this month
*/
// TODO
/*
Filtering different date ranges (hist only)
Upgrade warning
*/
export const SELECTORS = {
currentMonthActiveTab: '.active[data-test-current-month]',

View File

@ -1,12 +1,15 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { find, render, findAll, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { format, formatRFC3339, subMonths } from 'date-fns';
import { formatChartDate } from 'core/utils/date-formatters';
module('Integration | Component | clients/line-chart', function (hooks) {
setupRenderingTest(hooks);
const CURRENT_DATE = new Date();
hooks.beforeEach(function () {
this.set('xKey', 'foo');
this.set('yKey', 'bar');
this.set('dataset', [
{
foo: 1,
@ -30,11 +33,179 @@ module('Integration | Component | clients/line-chart', function (hooks) {
test('it renders', async function (assert) {
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart @dataset={{dataset}} @xKey="foo" @yKey="bar" />
<Clients::LineChart @dataset={{dataset}} @xKey={{xKey}} @yKey={{yKey}} />
</div>
`);
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
assert.dom('.hover-circle').exists({ count: 4 }, 'Renders dot for each data point');
assert
.dom('[data-test-line-chart="plot-point"]')
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
findAll('[data-test-line-chart="x-axis-labels"] text').forEach((e, i) => {
assert
.dom(e)
.hasText(`${this.dataset[i][this.xKey]}`, `renders x-axis label: ${this.dataset[i][this.xKey]}`);
});
assert.dom(find('[data-test-line-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
});
test('it renders upgrade data', async function (assert) {
this.set('dataset', [
{
foo: format(subMonths(CURRENT_DATE, 4), 'M/yy'),
bar: 4,
},
{
foo: format(subMonths(CURRENT_DATE, 3), 'M/yy'),
bar: 8,
},
{
foo: format(subMonths(CURRENT_DATE, 2), 'M/yy'),
bar: 14,
},
{
foo: format(subMonths(CURRENT_DATE, 1), 'M/yy'),
bar: 10,
},
]);
this.set('upgradeData', [
{
id: '1.10.1',
previousVersion: '1.9.2',
timestampInstalled: formatRFC3339(subMonths(CURRENT_DATE, 2)),
},
]);
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart
@dataset={{dataset}}
@upgradeData={{upgradeData}}
@xKey={{xKey}}
@yKey={{yKey}}
/>
</div>
`);
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
assert
.dom('[data-test-line-chart="plot-point"]')
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
assert
.dom(find(`[data-test-line-chart="upgrade-${this.dataset[2][this.xKey]}"]`))
.hasStyle({ opacity: '1' }, `upgrade data point ${this.dataset[2][this.xKey]} has yellow highlight`);
});
test('it renders tooltip', async function (assert) {
const tooltipData = [
{
month: format(subMonths(CURRENT_DATE, 4), 'M/yy'),
clients: 4,
new_clients: {
clients: 0,
},
},
{
month: format(subMonths(CURRENT_DATE, 3), 'M/yy'),
clients: 8,
new_clients: {
clients: 4,
},
},
{
month: format(subMonths(CURRENT_DATE, 2), 'M/yy'),
clients: 14,
new_clients: {
clients: 6,
},
},
{
month: format(subMonths(CURRENT_DATE, 1), 'M/yy'),
clients: 20,
new_clients: {
clients: 4,
},
},
];
this.set('dataset', tooltipData);
this.set('upgradeData', [
{
id: '1.10.1',
previousVersion: '1.9.2',
timestampInstalled: formatRFC3339(subMonths(CURRENT_DATE, 2)),
},
]);
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart
@dataset={{dataset}}
@upgradeData={{upgradeData}}
/>
</div>
`);
const tooltipHoverCircles = findAll('[data-test-line-chart] circle.hover-circle');
for (let [i, bar] of tooltipHoverCircles.entries()) {
await triggerEvent(bar, 'mouseover');
let tooltip = document.querySelector('.ember-modal-dialog');
let { month, clients, new_clients } = tooltipData[i];
assert
.dom(tooltip)
.includesText(
`${formatChartDate(month)} ${clients} total clients ${new_clients.clients} new clients`,
`tooltip text is correct for ${month}`
);
}
});
test('it fails gracefully when upgradeData is an object', async function (assert) {
this.set('upgradeData', { some: 'object' });
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart
@dataset={{dataset}}
@upgradeData={{upgradeData}}
@xKey={{xKey}}
@yKey={{yKey}}
/>
</div>
`);
assert
.dom('[data-test-line-chart="plot-point"]')
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData is not an array');
});
test('it fails gracefully when upgradeData has incorrect key names', async function (assert) {
this.set('upgradeData', [{ incorrect: 'key names' }]);
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart
@dataset={{dataset}}
@upgradeData={{upgradeData}}
@xKey={{xKey}}
@yKey={{yKey}}
/>
</div>
`);
assert
.dom('[data-test-line-chart="plot-point"]')
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData has incorrect keys');
});
test('it renders empty state when no dataset', async function (assert) {
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart @noDataMessage="this is a custom message to explain why you're not seeing a line chart"/>
</div>
`);
assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data');
assert
.dom('[data-test-empty-state-subtext]')
.hasText(
`this is a custom message to explain why you're not seeing a line chart`,
'custom message renders'
);
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, findAll, find, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/vertical-bar-chart', function (hooks) {
@ -12,19 +12,96 @@ module('Integration | Component | clients/vertical-bar-chart', function (hooks)
]);
});
test('it renders', async function (assert) {
test('it renders chart and tooltip for total clients', async function (assert) {
const barChartData = [
{ month: 'january', clients: 200, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
{ month: 'february', clients: 300, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
{ month: 'january', clients: 141, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
{ month: 'february', clients: 251, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
];
this.set('barChartData', barChartData);
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart
@dataset={{barChartData}}
@chartLegend={{chartLegend}}
/>
</div>
`);
assert.dom('[data-test-vertical-bar-chart]').exists();
const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
assert
.dom('[data-test-vertical-chart="data-bar"]')
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
});
for (let [i, bar] of tooltipHoverBars.entries()) {
await triggerEvent(bar, 'mouseover');
let tooltip = document.querySelector('.ember-modal-dialog');
assert
.dom(tooltip)
.includesText(
`${barChartData[i].clients} total clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
'tooltip text is correct'
);
}
});
test('it renders chart and tooltip for new clients', async function (assert) {
const barChartData = [
{ month: 'january', entity_clients: 91, non_entity_clients: 50, clients: 0 },
{ month: 'february', entity_clients: 101, non_entity_clients: 150, clients: 110 },
];
this.set('barChartData', barChartData);
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart
@dataset={{barChartData}}
@chartLegend={{chartLegend}}
/>
</div>
`);
const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
assert
.dom('[data-test-vertical-chart="data-bar"]')
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
});
for (let [i, bar] of tooltipHoverBars.entries()) {
await triggerEvent(bar, 'mouseover');
let tooltip = document.querySelector('.ember-modal-dialog');
assert
.dom(tooltip)
.includesText(
`${barChartData[i].clients} new clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
'tooltip text is correct'
);
}
});
test('it renders empty state when no dataset', async function (assert) {
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart @noDataMessage="this is a custom message to explain why you're not seeing a vertical bar chart"/>
</div>
`);
assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data');
assert
.dom('[data-test-empty-state-subtext]')
.hasText(
`this is a custom message to explain why you're not seeing a vertical bar chart`,
'custom message renders'
);
});
});

View File

@ -1,4 +1,4 @@
import { formatNumbers, formatTooltipNumber } from 'vault/utils/chart-helpers';
import { formatNumbers, formatTooltipNumber, calculateAverage } from 'vault/utils/chart-helpers';
import { module, test } from 'qunit';
const SMALL_NUMBERS = [0, 7, 27, 103, 999];
@ -28,4 +28,35 @@ module('Unit | Utility | chart-helpers', function () {
const formatted = formatTooltipNumber(120300200100);
assert.equal(formatted.length, 15, 'adds punctuation at proper place for large numbers');
});
test('calculateAverage is accurate', function (assert) {
const testArray1 = [
{ label: 'foo', value: 10 },
{ label: 'bar', value: 22 },
];
const testArray2 = [
{ label: 'foo', value: undefined },
{ label: 'bar', value: 22 },
];
const getAverage = (array) => array.reduce((a, b) => a + b, 0) / array.length;
assert.equal(calculateAverage(null), null, 'returns null if dataset it null');
assert.equal(calculateAverage([]), null, 'returns null if dataset it empty array');
assert.equal(calculateAverage([0]), getAverage([0]), `returns ${getAverage([0])} if array is just 0 0`);
assert.equal(calculateAverage([1]), getAverage([1]), `returns ${getAverage([1])} if array is just 1`);
assert.equal(
calculateAverage([5, 1, 41, 5]),
getAverage([5, 1, 41, 5]),
`returns correct average for array of integers`
);
assert.equal(
calculateAverage(testArray1, 'value'),
getAverage([10, 22]),
`returns correct average for array of objects`
);
assert.equal(
calculateAverage(testArray2, 'value'),
getAverage([0, 22]),
`returns correct average for array of objects containing undefined values`
);
});
});