UI/client count tests (#14162)

This commit is contained in:
Chelsea Shaw 2022-02-24 14:04:40 -06:00 committed by GitHub
parent a0101257ed
commit 9bb4920497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1369 additions and 302 deletions

View File

@ -78,6 +78,12 @@ long-form version of the npm script:
`ember server --proxy=http://localhost:PORT`
To run yarn with mirage, do:
- `yarn start:mirage handlername`
Where `handlername` is one of the options exported in `mirage/handlers/index`
### Code Generators
Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details)

View File

@ -39,6 +39,9 @@ export default class Attribution extends Component {
}
get isSingleNamespace() {
if (!this.args.totalClientsData) {
return 'no data';
}
// if a namespace is selected, then we're viewing top 10 auth methods (mounts)
return !!this.args.selectedNamespace;
}
@ -61,7 +64,16 @@ export default class Attribution extends Component {
get chartText() {
let dateText = this.isDateRange ? 'date range' : 'month';
if (!this.isSingleNamespace) {
switch (this.isSingleNamespace) {
case true:
return {
description:
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
${dateText === 'date range' ? ' over time.' : '.'}`,
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
};
case false:
return {
description:
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
@ -70,20 +82,12 @@ export default class Attribution extends Component {
${dateText === 'date range' ? ' over time.' : '.'}`,
totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`,
};
} else if (this.isSingleNamespace) {
return {
description:
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
${dateText === 'date range' ? ' over time.' : '.'}`,
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
};
} else {
case 'no data':
return {
description: 'There is a problem gathering data',
newCopy: 'There is a problem gathering data',
totalCopy: 'There is a problem gathering data',
};
default:
return '';
}
}

View File

@ -29,7 +29,11 @@ export default class Current extends Component {
}
get hasAttributionData() {
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod;
if (this.selectedAuthMethod) return false;
if (this.selectedNamespace) {
return this.authMethodOptions.length > 0;
}
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData;
}
get filteredActivity() {

View File

@ -62,10 +62,12 @@ export default class History extends Component {
// SEARCH SELECT
@tracked selectedNamespace = null;
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({
@tracked namespaceArray = this.getActivityResponse.byNamespace
? this.getActivityResponse.byNamespace.map((namespace) => ({
name: namespace.label,
id: namespace.label,
}));
}))
: [];
// TEMPLATE MESSAGING
@tracked noActivityDate = '';
@ -102,7 +104,11 @@ export default class History extends Component {
}
get hasAttributionData() {
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod;
if (this.selectedAuthMethod) return false;
if (this.selectedNamespace) {
return this.authMethodOptions.length > 0;
}
return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
}
get startTimeDisplay() {

View File

@ -21,8 +21,6 @@ import { tracked } from '@glimmer/tracking';
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
*/
// TODO CMB: delete original bar chart component
// SIZING CONSTANTS
const CHART_MARGIN = { top: 10, left: 95 }; // makes space for y-axis legend
const TRANSLATE = { down: 14, left: 99 };
@ -104,7 +102,7 @@ export default class HorizontalBarChart extends Component {
.append('rect')
.attr('class', 'data-bar')
.style('cursor', 'pointer')
.attr('width', (chartData) => `${xScale(chartData[1] - chartData[0])}%`)
.attr('width', (chartData) => `${xScale(Math.abs(chartData[1] - chartData[0]))}%`)
.attr('height', yScale.bandwidth())
.attr('x', (chartData) => `${xScale(chartData[0])}%`)
.attr('y', ({ data }) => yScale(data[labelKey]))

View File

@ -27,6 +27,14 @@ export default class LineChart extends Component {
@tracked tooltipTotal = '';
@tracked tooltipNew = '';
get yKey() {
return this.args.yKey || 'clients';
}
get xKey() {
return this.args.xKey || 'month';
}
@action removeTooltip() {
this.tooltipTarget = null;
}
@ -39,15 +47,17 @@ export default class LineChart extends Component {
// DEFINE AXES SCALES
let yScale = scaleLinear()
.domain([0, max(dataset.map((d) => d.clients))])
.range([0, 100]);
.domain([0, max(dataset.map((d) => d[this.yKey]))])
.range([0, 100])
.nice();
let yAxisScale = scaleLinear()
.domain([0, max(dataset.map((d) => d.clients))])
.range([SVG_DIMENSIONS.height, 0]);
.domain([0, max(dataset.map((d) => d[this.yKey]))])
.range([SVG_DIMENSIONS.height, 0])
.nice();
let xScale = scalePoint() // use scaleTime()?
.domain(dataset.map((d) => d.month))
.domain(dataset.map((d) => d[this.xKey]))
.range([0, SVG_DIMENSIONS.width])
.padding(0.2);
@ -67,8 +77,8 @@ export default class LineChart extends Component {
// PATH BETWEEN PLOT POINTS
let lineGenerator = line()
.x((d) => xScale(d.month))
.y((d) => yAxisScale(d.clients));
.x((d) => xScale(d[this.xKey]))
.y((d) => yAxisScale(d[this.yKey]));
chartSvg
.append('g')
@ -86,8 +96,8 @@ export default class LineChart extends Component {
.enter()
.append('circle')
.attr('class', 'data-plot')
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
.attr('cx', (d) => xScale(d.month))
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
.attr('cx', (d) => xScale(d[this.xKey]))
.attr('r', 3.5)
.attr('fill', LIGHT_AND_DARK_BLUE[0])
.attr('stroke', LIGHT_AND_DARK_BLUE[1])
@ -103,18 +113,19 @@ export default class LineChart extends Component {
.attr('class', 'hover-circle')
.style('cursor', 'pointer')
.style('opacity', '0')
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
.attr('cx', (d) => xScale(d.month))
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
.attr('cx', (d) => xScale(d[this.xKey]))
.attr('r', 10);
let hoverCircles = chartSvg.selectAll('.hover-circle');
// MOUSE EVENT FOR TOOLTIP
hoverCircles.on('mouseover', (data) => {
this.tooltipMonth = data.month;
this.tooltipTotal = `${data.clients} total clients`;
this.tooltipNew = `${data.new_clients.clients} new clients`;
let node = hoverCircles.filter((plot) => plot.month === data.month).node();
// TODO: how to genericize this?
this.tooltipMonth = data[this.xKey];
this.tooltipTotal = `${data[this.yKey]} total clients`;
this.tooltipNew = `${data.new_clients?.clients} new clients`;
let node = hoverCircles.filter((plot) => plot[this.xKey] === data[this.xKey]).node();
this.tooltipTarget = node;
});
}

View File

@ -47,7 +47,7 @@ export default class VerticalBarChart extends Component {
// DEFINE DATA BAR SCALES
let yScale = scaleLinear()
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
.domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data
.range([0, 100])
.nice();
@ -76,7 +76,7 @@ export default class VerticalBarChart extends Component {
// MAKE AXES //
let yAxisScale = scaleLinear()
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
.domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data
.range([`${SVG_DIMENSIONS.height}`, 0])
.nice();
@ -116,7 +116,7 @@ export default class VerticalBarChart extends Component {
// MOUSE EVENT FOR TOOLTIP
tooltipRect.on('mouseover', (data) => {
let hoveredMonth = data.month;
this.tooltipTotal = `${data.total} ${data.new_clients ? 'total' : 'new'} clients`;
this.tooltipTotal = `${data.clients} ${data.new_clients ? 'total' : 'new'} clients`;
this.uniqueEntities = `${data.entity_clients} unique entities`;
this.nonEntityTokens = `${data.non_entity_clients} non-entity tokens`;
// let node = chartSvg

View File

@ -8,7 +8,7 @@ import { tracked } from '@glimmer/tracking';
*
* @example
* ```js
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}}/>
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}} @submitText="Save"/>
* ```
* @param {function} handleDateSelection - is the action from the parent that the date picker triggers
* @param {string} [name] - optional argument passed from date dropdown to parent function

View File

@ -1,21 +1,17 @@
import Route from '@ember/routing/route';
import { isSameMonth } from 'date-fns';
import RSVP from 'rsvp';
import getStorage from 'vault/lib/token-storage';
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
export default class HistoryRoute extends Route {
async getActivity(start_time) {
try {
// on init ONLY make network request if we have a start time from the license
// otherwise user needs to manually input
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
} catch (e) {
// returns 400 when license start date is in the current month
if (e.httpStatus === 400) {
if (isSameMonth(new Date(start_time), new Date())) {
// triggers empty state to manually enter date if license begins in current month
return { isLicenseDateError: true };
}
throw e;
}
// on init ONLY make network request if we have a start_time
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
}
async getLicenseStartTime() {

View File

@ -9,8 +9,6 @@
.stacked-charts {
display: grid;
width: 100%;
// grid-template-columns: 1fr;
// grid-template-rows: 1fr;
}
.single-chart-grid {
@ -63,8 +61,8 @@
grid-row-start: 2;
grid-row-end: span 3;
justify-self: center;
height: 341px;
max-width: 730px;
height: 300px;
max-width: 700px;
svg.chart {
width: 100%;
@ -107,8 +105,13 @@
}
.chart-empty-state {
grid-column-start: 1;
grid-column-end: -1;
height: 100%;
width: 100%;
grid-row-end: span 3;
grid-column-end: span 3;
> div {
box-shadow: none !important;
}
}
.chart-subTitle {

View File

@ -1,18 +1,23 @@
<div class="chart-wrapper single-chart-grid">
<div class="chart-wrapper single-chart-grid" data-test-clients-attribution>
<div class="chart-header has-header-link has-bottom-margin-m">
<div class="header-left">
<h2 class="chart-title">Attribution</h2>
<p class="chart-description">{{this.chartText.description}}</p>
<p class="chart-description" data-test-attribution-description>{{this.chartText.description}}</p>
</div>
<div class="header-right">
{{#if @totalClientsData}}
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.showCSVDownloadModal) true)}}>
<button
data-test-attribution-export-button
type="button"
class="button is-secondary"
{{on "click" (fn (mut this.showCSVDownloadModal) true)}}
>
Export attribution data
</button>
{{/if}}
</div>
</div>
{{#if this.barChartTotalClients}}
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@ -20,30 +25,35 @@
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.totalCopy}}</p>
</div>
<div class="data-details-top">
<div class="data-details-top" data-test-top-attribution>
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-bottom">
<div class="data-details-bottom" data-test-top-counts>
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div>
<div class="timestamp">
Updated
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
</div>
<div class="legend-center">
<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>
{{else}}
<div class="chart-empty-state">
<EmptyState @icon="skip" @title="No data found" @bottomBorder={{true}} />
</div>
{{/if}}
<div class="timestamp" data-test-attribution-timestamp>
{{#if @timestamp}}
Updated
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
{{/if}}
</div>
</div>
{{! MODAL FOR CSV DOWNLOAD }}

View File

@ -22,7 +22,7 @@
<div class="is-subtitle-gray has-bottom-margin-m">
FILTERS
<Toolbar>
<ToolbarFilters>
<ToolbarFilters data-test-clients-filter-bar>
<SearchSelect
@id="namespace-search-select-monthly"
@options={{this.namespaceArray}}
@ -34,7 +34,7 @@
@displayInherit={{true}}
class="is-marginless"
/>
{{#if this.selectedNamespace}}
{{#if (not (is-empty this.authMethodOptions))}}
<SearchSelect
@id="auth-method-search-select"
@options={{this.authMethodOptions}}

View File

@ -9,7 +9,7 @@
</h2>
<div data-test-start-date-editor class="is-flex-align-baseline">
{{#if this.startTimeDisplay}}
<p class="is-size-6">{{this.startTimeDisplay}}</p>
<p class="is-size-6" data-test-date-display>{{this.startTimeDisplay}}</p>
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
Edit
</button>
@ -59,11 +59,10 @@
to enable tracking again.
</AlertBanner>
{{/if}}
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
{{#if (or this.totalUsageCounts this.hasAttributionData)}}
<div class="is-subtitle-gray has-bottom-margin-m">
FILTERS
<Toolbar>
<Toolbar data-test-clients-filter-bar>
<ToolbarFilters>
<CalendarWidget
@arrayOfMonths={{this.arrayOfMonths}}
@ -86,7 +85,7 @@
class="is-marginless"
/>
{{/if}}
{{#if this.selectedNamespace}}
{{#if (not (is-empty this.authMethodOptions))}}
<SearchSelect
@id="auth-method-search-select"
@options={{this.authMethodOptions}}

View File

@ -1,4 +1,9 @@
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.renderChart @dataset}}>
<svg
data-test-line-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
>
</svg>
{{! TOOLTIP }}

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 808 B

View File

@ -18,6 +18,7 @@
@value={{or @totalUsageCounts.clients "0"}}
@size="l"
@subText="The sum of entity clientIDs and non-entity clientIDs; this is Vaults primary billing metric."
data-test-stat-text="total-clients"
/>
</div>
<div class="column">
@ -27,6 +28,7 @@
@value={{or @totalUsageCounts.entity_clients "0"}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
data-test-stat-text="entity-clients"
/>
</div>
<div class="column">
@ -36,6 +38,7 @@
@value={{or @totalUsageCounts.non_entity_clients "0"}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity. "
data-test-stat-text="non-entity-clients"
/>
</div>
</div>

View File

@ -1,4 +1,9 @@
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.registerListener @dataset}}>
<svg
data-test-vertical-bar-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.registerListener @dataset}}
>
</svg>
{{! TOOLTIP }}

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 819 B

View File

@ -1,6 +1,6 @@
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
data-test-popup-menu-trigger="month"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
@ -9,7 +9,7 @@
</D.Trigger>
<D.Content @defaultClass="popup-menu-content is-wide">
<nav class="box menu scroll">
<ul class="menu-list">
<ul data-test-month-list class="menu-list">
{{#each this.months as |month index|}}
<button
type="button"
@ -26,7 +26,7 @@
</BasicDropdown>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
data-test-popup-menu-trigger="year"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
@ -35,7 +35,7 @@
</D.Trigger>
<D.Content @defaultClass="popup-menu-content is-wide">
<nav class="box menu">
<ul class="menu-list">
<ul data-test-year-list class="menu-list">
{{#each this.years as |year|}}
<button
type="button"
@ -51,6 +51,7 @@
</D.Content>
</BasicDropdown>
<button
data-test-date-dropdown-submit
type="button"
class="button is-primary"
disabled={{if (and this.startMonth this.startYear) false true}}

View File

@ -11,8 +11,10 @@ export const SVG_DIMENSIONS = { height: 190, width: 500 };
// Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g
export function formatNumbers(number) {
if (number < 1000) return number;
if (number < 10000) return format('.1s')(number);
// replace SI prefix of 'G' for billions to 'B'
return format('.1s')(number).replace('G', 'B');
return format('.2s')(number).replace('G', 'B');
}
export function formatTooltipNumber(value) {

View File

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

View File

@ -1,4 +1,4 @@
<nav class="toolbar">
<nav class="toolbar" ...attributes>
<div class="toolbar-scroller">
{{yield}}
</div>

View File

@ -34,12 +34,14 @@ export default function (server) {
server.get(
'/sys/internal/counters/activity',
function () {
function (_, req) {
const start_time = req.queryParams.start_time || '2021-03-17T00:00:00Z';
const end_time = req.queryParams.end_time || '2021-12-31T23:59:59Z';
return {
request_id: '26be5ab9-dcac-9237-ec12-269a8ca647d5',
data: {
start_time: '2021-03-17T00:00:00Z',
end_time: '2021-12-31T23:59:59Z',
start_time,
end_time,
total: {
_comment1: 'total client counts',
clients: 3637,

View File

@ -871,7 +871,7 @@ export default function (server) {
},
mounts: [
{
path: 'auth/method/uMGBU',
mount_path: 'auth/method/uMGBU',
counts: {
clients: 35,
entity_clients: 20,
@ -879,7 +879,7 @@ export default function (server) {
},
},
{
path: 'auth/method/woiej',
mount_path: 'auth/method/woiej',
counts: {
clients: 35,
entity_clients: 20,
@ -898,7 +898,7 @@ export default function (server) {
},
mounts: [
{
path: 'auth/method/ABCD1',
mount_path: 'auth/method/ABCD1',
counts: {
clients: 35,
entity_clients: 20,
@ -906,7 +906,7 @@ export default function (server) {
},
},
{
path: 'auth/method/ABCD2',
mount_path: 'auth/method/ABCD2',
counts: {
clients: 35,
entity_clients: 20,
@ -925,7 +925,7 @@ export default function (server) {
},
mounts: [
{
path: 'auth/method/XYZZ2',
mount_path: 'auth/method/XYZZ2',
counts: {
clients: 35,
entity_clients: 20,
@ -933,7 +933,7 @@ export default function (server) {
},
},
{
path: 'auth/method/XYZZ1',
mount_path: 'auth/method/XYZZ1',
counts: {
clients: 35,
entity_clients: 20,

View File

@ -0,0 +1,172 @@
import { module, test } from 'qunit';
import { visit, currentURL, settled, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import Pretender from 'pretender';
import authPage from 'vault/tests/pages/auth';
import { create } from 'ember-cli-page-object';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import ss from 'vault/tests/pages/components/search-select';
import {
generateConfigResponse,
generateCurrentMonthResponse,
SELECTORS,
sendResponse,
} from '../helpers/clients';
const searchSelect = create(ss);
module('Acceptance | clients current', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
return authPage.login();
});
hooks.afterEach(function () {
this.server.shutdown();
});
test('shows empty state when config disabled, no data', async function (assert) {
const config = generateConfigResponse({ enabled: 'default-disable' });
const monthly = generateCurrentMonthResponse();
this.server = new Pretender(function () {
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({}));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
});
await visit('/vault/clients/current');
assert.equal(currentURL(), '/vault/clients/current');
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
assert.dom(SELECTORS.emptyStateTitle).hasText('Tracking is disabled');
});
test('shows empty state when config enabled, no data', async function (assert) {
const config = generateConfigResponse();
const monthly = generateCurrentMonthResponse();
this.server = new Pretender(function () {
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/current');
assert.equal(currentURL(), '/vault/clients/current');
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
});
test('filters correctly on current with full data', async function (assert) {
const config = generateConfigResponse();
const monthly = generateCurrentMonthResponse(3);
this.server = new Pretender(function () {
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/current');
assert.equal(currentURL(), '/vault/clients/current');
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
const { clients, entity_clients, non_entity_clients } = monthly.data;
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
// Filter by namespace
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
// Filter by auth method
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
// Delete auth filter goes back to filtered only on namespace
await click('#auth-method-search-select [data-test-selected-list-button="delete"]');
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
// Delete namespace filter with auth filter on
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
// Goes back to no filters
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
});
test('filters correctly on current with no auth mounts', async function (assert) {
const config = generateConfigResponse();
const monthly = generateCurrentMonthResponse(3, true /* skip mounts */);
this.server = new Pretender(function () {
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/current');
assert.equal(currentURL(), '/vault/clients/current');
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
const { clients, entity_clients, non_entity_clients } = monthly.data;
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
// Filter by namespace
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution');
assert.dom('#auth-method-search-select').doesNotExist('Auth method filter is not shown');
// Remove namespace filter
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
// Goes back to no filters
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
});
});

View File

@ -0,0 +1,278 @@
import { module, test } from 'qunit';
import { visit, currentURL, click, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import Pretender from 'pretender';
import authPage from 'vault/tests/pages/auth';
import { addMonths, format, formatRFC3339, startOfMonth, subMonths } from 'date-fns';
import { create } from 'ember-cli-page-object';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import ss from 'vault/tests/pages/components/search-select';
import {
generateActivityResponse,
generateConfigResponse,
generateLicenseResponse,
SELECTORS,
sendResponse,
} from '../helpers/clients';
const searchSelect = create(ss);
module('Acceptance | clients history tab', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
return authPage.login();
});
hooks.afterEach(function () {
this.server.shutdown();
});
test('shows warning when config off, no data, queries available', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const license = generateLicenseResponse(licenseStart, licenseEnd);
const config = generateConfigResponse({ enabled: 'default-disable' });
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
assert.dom(SELECTORS.filterBar).doesNotExist('Shows filter bar to search previous dates');
assert.dom(SELECTORS.usageStats).doesNotExist('No usage stats');
});
test('shows warning when config off, no data, queries unavailable', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const license = generateLicenseResponse(licenseStart, licenseEnd);
const config = generateConfigResponse({ enabled: 'default-disable', queries_available: false });
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
assert.dom(SELECTORS.emptyStateTitle).hasText('Data tracking is disabled');
assert.dom(SELECTORS.filterBar).doesNotExist('Filter bar is hidden when no data available');
});
test('shows empty state when config on and no queries', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const license = generateLicenseResponse(licenseStart, licenseEnd);
const config = generateConfigResponse({ queries_available: false });
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
// History Tab
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
assert.dom(SELECTORS.emptyStateTitle).hasText('No monthly history');
assert.dom(SELECTORS.filterBar).doesNotExist('Does not show filter bar');
});
test('visiting history tab config on and data with mounts', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const lastMonth = addMonths(new Date(), -1);
const license = generateLicenseResponse(licenseStart, licenseEnd);
const config = generateConfigResponse();
const activity = generateActivityResponse(5, licenseStart, lastMonth);
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history');
assert
.dom(SELECTORS.dateDisplay)
.hasText(format(licenseStart, 'MMMM yyyy'), 'billing start month is correctly parsed from license');
assert
.dom(SELECTORS.rangeDropdown)
.hasText(
`${format(licenseStart, 'MMMM yyyy')} - ${format(lastMonth, 'MMMM yyyy')}`,
'Date range shows dates correctly parsed activity response'
);
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
const { clients, entity_clients, non_entity_clients } = activity.data.total;
assert
.dom('[data-test-stat-text="total-clients"] .stat-value')
.hasText(clients.toString(), 'total clients stat is correct');
assert
.dom('[data-test-stat-text="entity-clients"] .stat-value')
.hasText(entity_clients.toString(), 'entity clients stat is correct');
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString(), 'non-entity clients stat is correct');
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
});
test('filters correctly on history with full data', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const lastMonth = addMonths(new Date(), -1);
const config = generateConfigResponse();
const activity = generateActivityResponse(5, licenseStart, lastMonth);
const license = generateLicenseResponse(licenseStart, licenseEnd);
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
const { clients } = activity.data.total;
// Filter by namespace
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
assert.ok(true, 'Filter by first namespace');
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
// await this.pauseTest();
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
// Filter by auth method
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
assert.ok(true, 'Filter by first auth method');
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
await click('#namespace-search-select [data-test-selected-list-button="delete"]');
assert.ok(true, 'Remove namespace filter without first removing auth method filter');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert
.dom('[data-test-stat-text="total-clients"] .stat-value')
.hasText(clients.toString(), 'total clients stat is back to unfiltered value');
});
test('shows warning if upgrade happened within license period', async function (assert) {
const licenseStart = startOfMonth(subMonths(new Date(), 6));
const licenseEnd = addMonths(new Date(), 6);
const lastMonth = addMonths(new Date(), -1);
const config = generateConfigResponse();
const activity = generateActivityResponse(5, licenseStart, lastMonth);
const license = generateLicenseResponse(licenseStart, licenseEnd);
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () =>
sendResponse({
keys: ['1.9.0'],
key_info: {
'1.9.0': {
previous_version: '1.8.3',
timestamp_installed: formatRFC3339(addMonths(new Date(), -2)),
},
},
})
);
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
assert.dom('[data-test-flash-message] .message-actions').containsText(`You upgraded to Vault 1.9.0`);
});
test('Shows empty if license start date is current month', async function (assert) {
const licenseStart = new Date();
const licenseEnd = addMonths(new Date(), 12);
const config = generateConfigResponse();
const license = generateLicenseResponse(licenseStart, licenseEnd);
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(license));
this.get('/v1/sys/internal/counters/activity', () => this.passthrough);
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () =>
sendResponse({
keys: [],
})
);
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
assert.dom(SELECTORS.emptyStateTitle).hasText('No data for this billing period');
assert
.dom(SELECTORS.dateDisplay)
.hasText(format(licenseStart, 'MMMM yyyy'), 'Shows license date, gives ability to edit');
assert.dom('[data-test-popup-menu-trigger="month"]').exists('Dropdown exists to select month');
assert.dom('[data-test-popup-menu-trigger="year"]').exists('Dropdown exists to select year');
});
test('shows correct interface if no permissions on license', async function (assert) {
const config = generateConfigResponse();
this.server = new Pretender(function () {
this.get('/v1/sys/license/status', () => sendResponse(null, 403));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
this.get('/v1/sys/health', this.passthrough);
this.get('/v1/sys/seal-status', this.passthrough);
this.post('/v1/sys/capabilities-self', this.passthrough);
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
});
await visit('/vault/clients/history');
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
// Message changes depending on ent or OSS
assert.dom(SELECTORS.emptyStateTitle).exists('Empty state exists');
assert.dom('[data-test-popup-menu-trigger="month"]').exists('Dropdown exists to select month');
assert.dom('[data-test-popup-menu-trigger="year"]').exists('Dropdown exists to select year');
});
});

View File

@ -6,7 +6,7 @@ import logout from 'vault/tests/pages/logout';
import authForm from 'vault/tests/pages/components/auth-form';
import enablePage from 'vault/tests/pages/settings/auth/enable';
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
import { visit, settled, currentURL, find } from '@ember/test-helpers';
import { visit, settled, currentURL } from '@ember/test-helpers';
const consoleComponent = create(consoleClass);
const authFormComponent = create(authForm);
@ -156,10 +156,9 @@ module('Acceptance | oidc provider', function (hooks) {
await settled();
assert.equal(currentURL(), url, 'URL is as expected after login');
assert.dom('[data-test-oidc-redirect]').exists('redirect text exists');
assert.ok(
find('[data-test-oidc-redirect]').textContent.includes(`${callback}?code=`),
'Successful redirect to callback'
);
assert
.dom('[data-test-oidc-redirect]')
.hasTextContaining(`${callback}?code=`, 'Successful redirect to callback');
});
test('OIDC Provider redirects to auth if current token and prompt = login', async function (assert) {
@ -178,10 +177,9 @@ module('Acceptance | oidc provider', function (hooks) {
await authFormComponent.password(USER_PASSWORD);
await authFormComponent.login();
await settled();
assert.ok(
find('[data-test-oidc-redirect]').textContent.includes(`${callback}?code=`),
'Successful redirect to callback'
);
assert
.dom('[data-test-oidc-redirect]')
.hasTextContaining(`${callback}?code=`, 'Successful redirect to callback');
});
test('OIDC Provider shows consent form when prompt = consent', async function (assert) {

183
ui/tests/helpers/clients.js Normal file
View File

@ -0,0 +1,183 @@
import { formatRFC3339 } from 'date-fns';
/** Scenarios
* Config off, no data
* * queries available (hist only)
* * queries unavailable (hist only)
* Config on, no data
* Config on, with data
* Filtering (data with mounts)
* Filtering (data without mounts)
* -- HISTORY ONLY --
* No permissions for license
* Version (hist only)
* License start date this month
*/
export const SELECTORS = {
activeTab: '.nav-tab-link.is-active',
emptyStateTitle: '[data-test-empty-state-title]',
usageStats: '[data-test-usage-stats]',
dateDisplay: '[data-test-date-display]',
attributionBlock: '[data-test-clients-attribution]',
filterBar: '[data-test-clients-filter-bar]',
rangeDropdown: '[data-test-popup-menu-trigger]',
};
export function sendResponse(data, httpStatus = 200) {
if (httpStatus === 403) {
return [
httpStatus,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: ['permission denied'] }),
];
}
if (httpStatus === 204) {
// /activity endpoint returns 204 when no data, while
// /activity/monthly returns 200 with zero values on data
return [httpStatus, { 'Content-Type': 'application/json' }];
}
return [httpStatus, { 'Content-Type': 'application/json' }, JSON.stringify(data)];
}
export function generateConfigResponse(overrides = {}) {
return {
request_id: 'some-config-id',
data: {
default_report_months: 12,
enabled: 'default-enable',
queries_available: true,
retention_months: 24,
...overrides,
},
};
}
function generateNamespaceBlock(idx = 0, skipMounts = false) {
let mountCount = 1;
const nsBlock = {
namespace_id: `${idx}UUID`,
namespace_path: `my-namespace-${idx}/`,
counts: {
clients: mountCount * 15,
entity_clients: mountCount * 5,
non_entity_clients: mountCount * 10,
distinct_entities: mountCount * 5,
non_entity_tokens: mountCount * 10,
},
};
if (!skipMounts) {
mountCount = Math.floor((Math.random() + idx) * 20);
let mounts = [];
if (!skipMounts) {
Array.from(Array(mountCount)).forEach((v, index) => {
mounts.push({
mount_path: `auth/authid${index}`,
counts: {
clients: 5,
entity_clients: 3,
non_entity_clients: 2,
distinct_entities: 3,
non_entity_tokens: 2,
},
});
});
}
nsBlock.mounts = mounts;
}
return nsBlock;
}
export function generateActivityResponse(nsCount = 1, startDate, endDate) {
if (nsCount === 0) {
return {
request_id: 'some-activity-id',
data: {
start_time: formatRFC3339(startDate),
end_time: formatRFC3339(endDate),
total: {
clients: 0,
entity_clients: 0,
non_entity_clients: 0,
},
by_namespace: [
{
namespace_id: `root`,
namespace_path: '',
counts: {
entity_clients: 0,
non_entity_clients: 0,
clients: 0,
},
},
],
// months: [],
},
};
}
let namespaces = Array.from(Array(nsCount)).map((v, idx) => {
return generateNamespaceBlock(idx);
});
return {
request_id: 'some-activity-id',
data: {
start_time: formatRFC3339(startDate),
end_time: formatRFC3339(endDate),
total: {
clients: 999,
entity_clients: 666,
non_entity_clients: 333,
},
by_namespace: namespaces,
// months: [],
},
};
}
export function generateLicenseResponse(startDate, endDate) {
return {
request_id: 'my-license-request-id',
data: {
autoloaded: {
license_id: 'my-license-id',
start_time: formatRFC3339(startDate),
expiration_time: formatRFC3339(endDate),
},
},
};
}
export function generateCurrentMonthResponse(namespaceCount, skipMounts = false) {
if (!namespaceCount) {
return {
request_id: 'monthly-response-id',
data: {
by_namespace: [],
clients: 0,
entity_clients: 0,
non_entity_clients: 0,
},
};
}
// generate by_namespace data
const by_namespace = Array.from(Array(namespaceCount)).map((ns, idx) =>
generateNamespaceBlock(idx, skipMounts)
);
const counts = by_namespace.reduce(
(prev, curr) => {
return {
clients: prev.clients + curr.counts.clients,
entity_clients: prev.entity_clients + curr.counts.entity_clients,
non_entity_clients: prev.non_entity_clients + curr.counts.non_entity_clients,
};
},
{ clients: 0, entity_clients: 0, non_entity_clients: 0 }
);
return {
request_id: 'monthly-response-id',
data: {
by_namespace,
...counts,
},
};
}

View File

@ -1,69 +0,0 @@
import { module, test } from 'qunit';
import EmberObject from '@ember/object';
import { render } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | client count current', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
let model = EmberObject.create({
config: {},
monthly: {},
versionHistory: [],
});
this.model = model;
});
test('it shows empty state when disabled and no data available', async function (assert) {
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Current @model={{this.model}} />
`);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('Tracking is disabled');
});
test('it shows empty state when enabled and no data', async function (assert) {
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Current @model={{this.model}} />`);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('No data received');
});
test('it shows zeroed data when enabled but no counts', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' });
Object.assign(this.model.monthly, {
byNamespace: [{ label: 'root', clients: 0, entity_clients: 0, non_entity_clients: 0 }],
total: { clients: 0, entity_clients: 0, non_entity_clients: 0 },
});
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Current @model={{this.model}} />
`);
assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty state does not exist');
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
assert.dom('[data-test-stat-text-container]').includesText('0');
});
test('it shows data when available from query', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
Object.assign(this.model.monthly, {
total: {
clients: 1234,
entity_clients: 234,
non_entity_clients: 232,
},
});
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Current @model={{this.model}} />`);
assert.dom('[data-test-pricing-metrics-form]').doesNotExist('Date range component should not exists');
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
});
});

View File

@ -1,103 +0,0 @@
import { module, test } from 'qunit';
import EmberObject from '@ember/object';
import { render } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | client count history', function (hooks) {
// TODO CMB add tests for calendar widget showing
setupRenderingTest(hooks);
hooks.beforeEach(function () {
let model = EmberObject.create({
config: {},
activity: {},
versionHistory: [],
});
this.model = model;
});
test('it shows empty state when disabled and no data available', async function (assert) {
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::History @model={{this.model}} />`);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('Data tracking is disabled');
});
test('it shows empty state when enabled and no data available', async function (assert) {
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::History @model={{this.model}} />`);
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('No monthly history');
});
test('it shows empty state when no data for queried date range', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true });
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
Object.assign(this.model.activity, {
byNamespace: [
{
label: 'namespace24/',
clients: 8301,
entity_clients: 4387,
non_entity_clients: 3914,
mounts: [],
},
{
label: 'namespace88/',
clients: 7752,
entity_clients: 3632,
non_entity_clients: 4120,
mounts: [],
},
],
});
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::History @model={{this.model}} />`);
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
assert.dom('[data-test-empty-state-title]').hasText('No data received');
});
test('it shows warning when disabled and data available', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'Off' });
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::History @model={{this.model}} />`);
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
assert.dom('[data-test-tracking-disabled]').exists('Flash message exists');
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
});
test('it shows data when available from query', async function (assert) {
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
Object.assign(this.model.activity, {
byNamespace: [
{ label: 'nsTest5/', clients: 2725, entity_clients: 1137, non_entity_clients: 1588 },
{ label: 'nsTest1/', clients: 200, entity_clients: 100, non_entity_clients: 100 },
],
total: {
clients: 1234,
entity_clients: 234,
non_entity_clients: 232,
},
});
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::History @model={{this.model}} />`);
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
assert.dom('[data-test-horizontal-bar-chart]').exists('Horizontal bar chart exists');
});
});

View File

@ -0,0 +1,144 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { formatRFC3339 } from 'date-fns';
import { click } from '@ember/test-helpers';
module('Integration | Component | clients/attribution', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('timestamp', formatRFC3339(new Date()));
this.set('selectedNamespace', null);
this.set('isDateRange', true);
this.set('chartLegend', [
{ label: 'entity clients', key: 'entity_clients' },
{ label: 'non-entity clients', key: 'non_entity_clients' },
]);
this.set('totalUsageCounts', { clients: 15, entity_clients: 10, non_entity_clients: 5 });
this.set('totalClientsData', [
{ label: 'second', clients: 10, entity_clients: 7, non_entity_clients: 3 },
{ label: 'first', clients: 5, entity_clients: 3, non_entity_clients: 2 },
]);
this.set('totalMountsData', { clients: 5, entity_clients: 3, non_entity_clients: 2 });
this.set('namespaceMountsData', [
{ label: 'auth1/', clients: 3, entity_clients: 2, non_entity_clients: 1 },
{ label: 'auth2/', clients: 2, entity_clients: 1, non_entity_clients: 1 },
]);
});
test('it renders empty state with no data', async function (assert) {
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution @chartLegend={{chartLegend}} />
`);
assert.dom('[data-test-component="empty-state"]').exists();
assert.dom('[data-test-empty-state-title]').hasText('No data found');
assert.dom('[data-test-attribution-description]').hasText('There is a problem gathering data');
assert.dom('[data-test-attribution-export-button]').doesNotExist();
assert.dom('[data-test-attribution-timestamp]').doesNotHaveTextContaining('Updated');
});
test('it renders with data for namespaces', async function (assert) {
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{totalClientsData}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@isDateRange={{isDateRange}}
/>
`);
assert.dom('[data-test-component="empty-state"]').doesNotExist();
assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays');
assert.dom('[data-test-attribution-export-button]').exists();
assert
.dom('[data-test-attribution-description]')
.hasText(
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.'
);
assert
.dom('[data-test-attribution-subtext]')
.hasText(
'The total clients in the namespace for this date range. This number is useful for identifying overall usage volume.'
);
assert.dom('[data-test-top-attribution]').includesText('namespace').includesText('second');
assert.dom('[data-test-top-counts]').includesText('namespace').includesText('10');
});
test('it renders correct text for a single month', async function (assert) {
this.set('isDateRange', false);
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{totalClientsData}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@isDateRange={{isDateRange}}
/>
`);
assert
.dom('[data-test-attribution-subtext]')
.includesText('namespace for this month', 'renders monthly namespace text');
this.set('selectedNamespace', 'second');
assert
.dom('[data-test-attribution-subtext]')
.includesText('auth method for this month', 'renders monthly auth method text');
});
test('it renders with data for selected namespace auth methods for a date range', async function (assert) {
this.set('selectedNamespace', 'second');
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{namespaceMountsData}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@isDateRange={{isDateRange}}
/>
`);
assert.dom('[data-test-component="empty-state"]').doesNotExist();
assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays');
assert.dom('[data-test-attribution-export-button]').exists();
assert
.dom('[data-test-attribution-description]')
.hasText(
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.'
);
assert
.dom('[data-test-attribution-subtext]')
.hasText(
'The total clients used by the auth method for this date range. This number is useful for identifying overall usage volume.'
);
assert.dom('[data-test-top-attribution]').includesText('auth method').includesText('auth1/');
assert.dom('[data-test-top-counts]').includesText('auth method').includesText('3');
});
test('it renders modal', async function (assert) {
await render(hbs`
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{namespaceMountsData}}
@timestamp={{timestamp}}
@startTimeDisplay={{"January 2022"}}
@endTimeDisplay={{"February 2022"}}
/>
`);
await click('[data-test-attribution-export-button]');
assert.dom('.modal.is-active .title').hasText('Export attribution data', 'modal appears to export csv');
assert.dom('.modal.is-active').includesText('January 2022 - February 2022');
});
});

View File

@ -0,0 +1,85 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { findAll, render, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/horizontal-bar-chart', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('chartLegend', [
{ label: 'entity clients', key: 'entity_clients' },
{ label: 'non-entity clients', key: 'non_entity_clients' },
]);
});
test('it renders chart and tooltip', async function (assert) {
const totalObject = { clients: 5, entity_clients: 2, non_entity_clients: 3 };
const dataArray = [
{ label: 'second', clients: 3, entity_clients: 1, non_entity_clients: 2 },
{ label: 'first', clients: 2, entity_clients: 1, non_entity_clients: 1 },
];
this.set('totalUsageCounts', totalObject);
this.set('totalClientsData', dataArray);
await render(hbs`
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{chartLegend}}
@totalUsageCounts={{totalUsageCounts}}
/>`);
assert.dom('[data-test-horizontal-bar-chart]').exists();
const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar');
const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar');
assert.equal(actionBars.length, dataArray.length, 'renders correct number of hover bars');
assert.equal(dataBars.length, dataArray.length * 2, 'renders correct number of data bars');
const textLabels = this.element.querySelectorAll('[data-test-horizontal-bar-chart] .tick text');
const textTotals = this.element.querySelectorAll('[data-test-horizontal-bar-chart] text.total-value');
textLabels.forEach((label, index) => {
assert.dom(label).hasText(dataArray[index].label, 'label renders correct text');
});
textTotals.forEach((label, index) => {
assert.dom(label).hasText(`${dataArray[index].clients}`, 'total value renders correct number');
});
for (let [i, bar] of actionBars.entries()) {
let percent = Math.round((dataArray[i].clients / totalObject.clients) * 100);
await triggerEvent(bar, 'mouseover');
let tooltip = document.querySelector('.ember-modal-dialog');
assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage');
}
});
test('it renders data with a large range', async function (assert) {
const totalObject = { clients: 5929393, entity_clients: 1391997, non_entity_clients: 4537396 };
const dataArray = [
{ label: 'second', clients: 5929093, entity_clients: 1391896, non_entity_clients: 4537100 },
{ label: 'first', clients: 300, entity_clients: 101, non_entity_clients: 296 },
];
this.set('totalUsageCounts', totalObject);
this.set('totalClientsData', dataArray);
await render(hbs`
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{chartLegend}}
@totalUsageCounts={{totalUsageCounts}}
/>`);
assert.dom('[data-test-horizontal-bar-chart]').exists();
const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar');
const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar');
assert.equal(actionBars.length, dataArray.length, 'renders correct number of hover bars');
assert.equal(dataBars.length, dataArray.length * 2, 'renders correct number of data bars');
for (let [i, bar] of actionBars.entries()) {
let percent = Math.round((dataArray[i].clients / totalObject.clients) * 100);
await triggerEvent(bar, 'mouseover');
let tooltip = document.querySelector('.ember-modal-dialog');
assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage');
}
});
});

View File

@ -0,0 +1,40 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/line-chart', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('dataset', [
{
foo: 1,
bar: 4,
},
{
foo: 2,
bar: 8,
},
{
foo: 3,
bar: 14,
},
{
foo: 4,
bar: 10,
},
]);
});
test('it renders', async function (assert) {
await render(hbs`
<div class="chart-container-wide">
<Clients::LineChart @dataset={{dataset}} @xKey="foo" @yKey="bar" />
</div>
`);
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
assert.dom('.hover-circle').exists({ count: 4 }, 'Renders dot for each data point');
});
});

View File

@ -0,0 +1,46 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/usage-stats', function (hooks) {
setupRenderingTest(hooks);
test('it renders defaults', async function (assert) {
await render(hbs`<Clients::UsageStats />`);
assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed');
assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists');
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('0', 'Value defaults to zero');
assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('0', 'Value defaults to zero');
assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists');
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText('0', 'Value defaults to zero');
assert.dom('a').hasAttribute('href', 'https://learn.hashicorp.com/tutorials/vault/usage-metrics');
});
test('it renders with data', async function (assert) {
this.set('counts', {
clients: 17,
entity_clients: 7,
non_entity_clients: 10,
});
await render(hbs`<Clients::UsageStats @totalUsageCounts={{counts}} />`);
assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed');
assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists');
assert
.dom('[data-test-stat-text="total-clients"] .stat-value')
.hasText('17', 'Total clients shows passed value');
assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists');
assert
.dom('[data-test-stat-text="entity-clients"] .stat-value')
.hasText('7', 'entity clients shows passed value');
assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists');
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText('10', 'non entity clients shows passed value');
});
});

View File

@ -0,0 +1,30 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/vertical-bar-chart', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('chartLegend', [
{ label: 'entity clients', key: 'entity_clients' },
{ label: 'non-entity clients', key: 'non_entity_clients' },
]);
});
test('it renders', 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 },
];
this.set('barChartData', barChartData);
await render(hbs`
<Clients::VerticalBarChart
@dataset={{barChartData}}
@chartLegend={{chartLegend}}
/>
`);
assert.dom('[data-test-vertical-bar-chart]').exists();
});
});

View File

@ -0,0 +1,174 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
const ARRAY_OF_MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const CURRENT_DATE = new Date();
const CURRENT_YEAR = CURRENT_DATE.getFullYear(); // integer of year
const CURRENT_MONTH = CURRENT_DATE.getMonth(); // index of month
module('Integration | Component | date-dropdown', function (hooks) {
setupRenderingTest(hooks);
test('it renders dropdown', async function (assert) {
this.set('text', 'Save');
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown/>
</div>
`);
assert.dom('[data-test-date-dropdown-submit]').hasText('Submit', 'button renders default text');
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown @submitText={{text}}/>
</div>
`);
assert.dom('[data-test-date-dropdown-submit]').hasText('Save', 'button renders passed in text');
});
test('it renders dropdown and selects month and year', async function (assert) {
let parentAction = (month, year) => {
assert.equal(month, 'January', 'sends correct month to parent callback');
assert.equal(year, CURRENT_YEAR, 'sends correct year to parent callback');
};
this.set('parentAction', parentAction);
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown
@handleDateSelection={{parentAction}} />
</div>
`);
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
const submitButton = this.element.querySelector('[data-test-date-dropdown-submit]');
assert.strictEqual(submitButton.disabled, true, 'button is disabled when no month or year selected');
await click(monthDropdown);
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
assert.equal(dropdownListMonths.length, 12, 'dropdown has 12 months');
for (let [index, month] of ARRAY_OF_MONTHS.entries()) {
assert.dom(dropdownListMonths[index]).hasText(`${month}`, `dropdown includes ${month}`);
}
await click(dropdownListMonths[0]);
assert.dom(monthDropdown).hasText('January', 'dropdown selects January');
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting month');
await click(yearDropdown);
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
assert.equal(dropdownListYears.length, 5, 'dropdown has 5 years');
for (let [index, year] of dropdownListYears.entries()) {
let comparisonYear = CURRENT_YEAR - index;
assert.dom(year).hasText(`${comparisonYear}`, `dropdown includes ${comparisonYear}`);
}
await click(dropdownListYears[0]);
assert.dom(yearDropdown).hasText(`${CURRENT_YEAR}`, `dropdown selects ${CURRENT_YEAR}`);
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting year');
assert.strictEqual(submitButton.disabled, false, 'button enabled when month and year selected');
await click(submitButton);
});
test('it disables correct years when selecting month first', async function (assert) {
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown/>
</div>
`);
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
for (let i = 0; i < 12; i++) {
await click(monthDropdown);
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
await click(dropdownListMonths[i]);
await click(yearDropdown);
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
if (i < CURRENT_MONTH) {
for (let year of dropdownListYears) {
assert.strictEqual(year.disabled, false, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
}
} else {
for (let [yearIndex, year] of dropdownListYears.entries()) {
if (yearIndex === 0) {
assert.strictEqual(year.disabled, true, `${ARRAY_OF_MONTHS[i]} ${year.innerText} disabled`);
} else {
assert.strictEqual(year.disabled, false, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
}
}
}
await click(yearDropdown);
}
});
test('it disables correct months when selecting year first', async function (assert) {
await render(hbs`
<div class="is-flex-align-baseline">
<DateDropdown/>
</div>
`);
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
for (let i = 0; i < 5; i++) {
await click(yearDropdown);
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
await click(dropdownListYears[i]);
await click(monthDropdown);
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
if (i === 0) {
for (let [monthIndex, month] of dropdownListMonths.entries()) {
if (monthIndex < CURRENT_MONTH) {
assert.strictEqual(
month.disabled,
false,
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
);
} else {
assert.strictEqual(
month.disabled,
true,
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} disabled`
);
}
}
} else {
for (let [monthIndex, month] of dropdownListMonths.entries()) {
assert.strictEqual(
month.disabled,
false,
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
);
}
}
await click(monthDropdown);
}
});
});

View File

@ -0,0 +1,30 @@
import { formatNumbers, formatTooltipNumber } from 'vault/utils/chart-helpers';
import { module, test } from 'qunit';
const SMALL_NUMBERS = [0, 7, 27, 103, 999];
const LARGE_NUMBERS = {
1001: '1k',
33777: '34k',
532543: '530k',
2100100: '2.1M',
54500200100: '55B',
};
module('Unit | Utility | chart-helpers', function () {
test('formatNumbers renders number correctly', function (assert) {
const method = formatNumbers();
assert.ok(method);
SMALL_NUMBERS.forEach(function (num) {
assert.equal(formatNumbers(num), num, `Does not format small number ${num}`);
});
Object.keys(LARGE_NUMBERS).forEach(function (num) {
const expected = LARGE_NUMBERS[num];
assert.equal(formatNumbers(num), expected, `Formats ${num} as ${expected}`);
});
});
test('formatTooltipNumber renders number correctly', function (assert) {
const formatted = formatTooltipNumber(120300200100);
assert.equal(formatted.length, 15, 'adds punctuation at proper place for large numbers');
});
});