mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-18 20:31:08 +02:00
* rename query params to match keys from api response * move type guard check to util * update color scheme * remove passing selected namespace to export activity data request * remove namespace and mount filter toolbar * update response, sort by client count number * remove default page size for testing * implement table and filters in overview tab * remove old query params * cleanup unused args * revert page header changes * update mirage, remove month from utils * update client count utils * one more color! * reset table to page 1; * workaround to force Hds::Pagination::Numbered to update when currentPage changes * add empty state test for no attribution * delete unused methods * add test for new utils * add changelog * rename changelog --------- Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
232 lines
10 KiB
JavaScript
232 lines
10 KiB
JavaScript
/**
|
||
* Copyright (c) HashiCorp, Inc.
|
||
* SPDX-License-Identifier: BUSL-1.1
|
||
*/
|
||
|
||
import { module, test } from 'qunit';
|
||
import { setupRenderingTest } from 'ember-qunit';
|
||
import { click, fillIn, findAll, render, waitFor } from '@ember/test-helpers';
|
||
import { hbs } from 'ember-cli-htmlbars';
|
||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
|
||
|
||
const MOCK_DATA = [
|
||
{ island: 'Maldives', visit_length: 5, is_booked: false, trip_date: '2025-06-22T00:00:00.000Z' },
|
||
{ island: 'Bora Bora', visit_length: 7, is_booked: true, trip_date: '2025-03-15T00:00:00.000Z' },
|
||
{ island: 'Fiji', visit_length: 10, is_booked: true, trip_date: '2025-09-08T00:00:00.000Z' },
|
||
{ island: 'Santorini', visit_length: 4, is_booked: false, trip_date: '2026-04-10T00:00:00.000Z' },
|
||
{ island: 'Maui', visit_length: 8, is_booked: true, trip_date: '2026-01-18T00:00:00.000Z' },
|
||
{ island: 'Seychelles', visit_length: 6, is_booked: false, trip_date: '2025-12-03T00:00:00.000Z' },
|
||
];
|
||
module('Integration | Component | clients/table', function (hooks) {
|
||
setupRenderingTest(hooks);
|
||
|
||
hooks.beforeEach(async function () {
|
||
this.data = undefined;
|
||
this.columns = [
|
||
{ key: 'island', label: 'Islands', isSortable: true },
|
||
{ key: 'visit_length', label: 'Visit length', isSortable: true },
|
||
{ key: 'is_booked', label: 'Vacation booking status', isSortable: true },
|
||
{ key: 'trip_date', label: 'Date trip starts', isSortable: true },
|
||
];
|
||
this;
|
||
this.initiallySortBy = undefined;
|
||
this.setPageSize = undefined;
|
||
this.showPaginationSizeSelector = undefined;
|
||
// helper function to setup table with X number of simple objects for pagination tests
|
||
this.mockMoreData = (recordNumber) => {
|
||
const record = (i) => ({ id: i, name: `record-${i}` });
|
||
this.columns = [
|
||
{ key: 'id', label: 'ID', isSortable: true },
|
||
{ key: 'name', label: 'Name', isSortable: true },
|
||
];
|
||
this.data = Array.from({ length: recordNumber }, (_, i) => record(i));
|
||
};
|
||
this.renderComponent = async () => {
|
||
return render(hbs`
|
||
<Clients::Table
|
||
@columns={{this.columns}}
|
||
@data={{this.data}}
|
||
@initiallySortBy={{this.initiallySortBy}}
|
||
@setPageSize={{this.setPageSize}}
|
||
@showPaginationSizeSelector={{this.showPaginationSizeSelector}}
|
||
/>`);
|
||
};
|
||
});
|
||
|
||
test('it renders default empty state when no data exists', async function (assert) {
|
||
await this.renderComponent();
|
||
assert.dom(CLIENT_COUNT.card('table empty state')).hasText('No data to display');
|
||
});
|
||
|
||
test('it renders yielded empty state block when no data exists', async function (assert) {
|
||
await render(hbs`
|
||
<Clients::Table @columns={{this.columns}} @data={{this.data}}>
|
||
<:emptyState>Oh no, there's no data!</:emptyState>
|
||
</Clients::Table>`);
|
||
assert.dom(CLIENT_COUNT.card('table empty state')).hasText("Oh no, there's no data!");
|
||
});
|
||
|
||
test('it renders and paginates data', async function (assert) {
|
||
this.data = MOCK_DATA;
|
||
await this.renderComponent();
|
||
assert.dom(CLIENT_COUNT.card('table empty state')).doesNotExist();
|
||
assert.dom(GENERAL.paginationInfo).hasText(`1–5 of ${this.data.length}`);
|
||
await click(GENERAL.nextPage);
|
||
assert.dom(GENERAL.tableData(0, 'island')).hasText('Seychelles', 'it paginates the data');
|
||
});
|
||
|
||
test('it sorts table data', async function (assert) {
|
||
this.data = MOCK_DATA;
|
||
const assertSortOrder = (expectedValues, { column, page }) => {
|
||
expectedValues.forEach((value, idx) => {
|
||
assert
|
||
.dom(GENERAL.tableData(idx, column))
|
||
.hasText(value, `page ${page}, row ${idx} has ${column}: ${value}`);
|
||
});
|
||
};
|
||
|
||
await this.renderComponent();
|
||
const [firstColumn, secondColumn, thirdColumn, fourthColumn] = findAll(GENERAL.icon('swap-vertical'));
|
||
await click(firstColumn);
|
||
assertSortOrder(['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini'], { column: 'island', page: 1 });
|
||
await click(GENERAL.nextPage);
|
||
assertSortOrder(['Seychelles'], { column: 'island', page: 2 });
|
||
|
||
await click(GENERAL.prevPage);
|
||
await click(secondColumn);
|
||
assertSortOrder(['4', '5', '6', '7', '8'], { column: 'visit_length', page: 1 });
|
||
await click(GENERAL.nextPage);
|
||
assertSortOrder(['10'], { column: 'visit_length', page: 2 });
|
||
|
||
await click(GENERAL.prevPage);
|
||
await click(thirdColumn);
|
||
assertSortOrder(['false', 'false', 'false', 'true', 'true'], { column: 'is_booked', page: 1 });
|
||
await click(GENERAL.nextPage);
|
||
assertSortOrder(['true'], { column: 'is_booked', page: 2 });
|
||
|
||
await click(GENERAL.prevPage);
|
||
await click(fourthColumn);
|
||
assertSortOrder(
|
||
[
|
||
'2025-03-15T00:00:00.000Z',
|
||
'2025-06-22T00:00:00.000Z',
|
||
'2025-09-08T00:00:00.000Z',
|
||
'2025-12-03T00:00:00.000Z',
|
||
'2026-01-18T00:00:00.000Z',
|
||
],
|
||
{
|
||
column: 'trip_date',
|
||
page: 1,
|
||
}
|
||
);
|
||
await click(GENERAL.nextPage);
|
||
assertSortOrder(['2026-04-10T00:00:00.000Z'], {
|
||
column: 'trip_date',
|
||
page: 2,
|
||
});
|
||
});
|
||
|
||
test('it pre-sorts table data if @initiallySortBy is set', async function (assert) {
|
||
this.data = MOCK_DATA;
|
||
this.initiallySortBy = { column: 'visit_length', direction: 'desc' };
|
||
await this.renderComponent();
|
||
assert.dom(GENERAL.tableColumnHeader(2, { isAdvanced: true })).hasAttribute('aria-sort', 'descending');
|
||
assert
|
||
.dom(`${GENERAL.tableColumnHeader(2, { isAdvanced: true })} ${GENERAL.icon('arrow-down')}`)
|
||
.exists();
|
||
const firstPage = ['Fiji', 'Maui', 'Bora Bora', 'Seychelles', 'Maldives'];
|
||
firstPage.forEach((value, idx) => {
|
||
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 1, row ${idx} has ${value}`);
|
||
});
|
||
await click(GENERAL.nextPage);
|
||
const secondPage = ['Santorini'];
|
||
secondPage.forEach((value, idx) => {
|
||
assert.dom(GENERAL.tableData(idx, 'island')).hasText(value, `page 2, row ${idx} has ${value}`);
|
||
});
|
||
});
|
||
|
||
test('it sets page size if @setPageSize has a value', async function (assert) {
|
||
this.mockMoreData(15);
|
||
this.setPageSize = 8; // component default for testing is 3, so set to anything but 3
|
||
await this.renderComponent();
|
||
assert.dom(GENERAL.paginationInfo).hasText(`1–${this.setPageSize} of ${this.data.length}`);
|
||
let idx = 7; // 8th item, table items are 0-indexed
|
||
assert
|
||
.dom(GENERAL.tableData(idx, 'name'))
|
||
.hasText(this.data[idx].name, 'last row is 8th item in dataset');
|
||
await click(GENERAL.nextPage);
|
||
idx = 8; // 9th item, table items are 0-indexed
|
||
assert
|
||
.dom(GENERAL.tableData(0, 'name'))
|
||
.hasText(this.data[idx].name, 'first row on page 2 is 9th item in dataset');
|
||
});
|
||
|
||
test('it renders size selector if @showPaginationSizeSelector is true', async function (assert) {
|
||
this.mockMoreData(10);
|
||
this.setPageSize = 5;
|
||
this.showPaginationSizeSelector = true;
|
||
await this.renderComponent();
|
||
|
||
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, '5 rows render');
|
||
assert.dom(GENERAL.paginationInfo).hasText(`1–${this.setPageSize} of ${this.data.length}`);
|
||
assert.dom(GENERAL.paginationSizeSelector).hasValue('5');
|
||
let idx = 4; // rows and ids are 0-indexed
|
||
assert.dom(GENERAL.tableData(idx, 'id')).hasText(`${idx}`, `last row is ${idx + 1}th item in dataset`);
|
||
await fillIn(GENERAL.paginationSizeSelector, '10');
|
||
idx = 9;
|
||
assert.dom(GENERAL.tableRow()).exists({ count: 10 }, '10 rows render');
|
||
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'it updates the size selector to 10');
|
||
assert.dom(GENERAL.tableData(idx, 'id')).hasText(`${idx}`, `last row is ${idx + 1}th item in dataset`);
|
||
});
|
||
|
||
test('it renders "Deleted" badge for "mount_type" keys if the value is "deleted mount"', async function (assert) {
|
||
this.columns = [
|
||
{ key: 'mount_type', label: 'Mount type' },
|
||
{ key: 'mount_path', label: 'Mount path' },
|
||
];
|
||
this.data = [
|
||
{ mount_type: 'deleted mount', mount_path: 'auth/userpass/' },
|
||
{ mount_type: 'ns_token', mount_path: 'auth/token/' },
|
||
];
|
||
await this.renderComponent();
|
||
assert.dom('.hds-badge').exists({ count: 1 }, 'only one badge renders');
|
||
assert
|
||
.dom(`${GENERAL.tableData(0, 'mount_type')} .hds-badge`)
|
||
.exists('it renders a badge for the deleted mount')
|
||
.hasText('Deleted');
|
||
});
|
||
|
||
test('it resets pagination when data changes', async function (assert) {
|
||
// We need more than 5 rows, so here's more mock data!
|
||
const moreData = [
|
||
{ island: 'Tahiti', visit_length: 12, is_booked: true, trip_date: '2025-05-10T00:00:00.000Z' },
|
||
{ island: 'Barbados', visit_length: 6, is_booked: false, trip_date: '2025-08-25T00:00:00.000Z' },
|
||
{ island: 'Cyprus', visit_length: 9, is_booked: true, trip_date: '2026-03-12T00:00:00.000Z' },
|
||
{ island: 'Jamaica', visit_length: 7, is_booked: false, trip_date: '2025-11-05T00:00:00.000Z' },
|
||
{ island: 'Crete', visit_length: 11, is_booked: true, trip_date: '2026-06-18T00:00:00.000Z' },
|
||
{ island: 'Aruba', visit_length: 5, is_booked: false, trip_date: '2025-10-14T00:00:00.000Z' },
|
||
];
|
||
this.data = [...MOCK_DATA, ...moreData];
|
||
this.showPaginationSizeSelector = true;
|
||
await this.renderComponent();
|
||
await fillIn(GENERAL.paginationSizeSelector, '10'); // Default is 5, so change to something else
|
||
await click(GENERAL.nextPage);
|
||
assert.dom(GENERAL.paginationInfo).hasText(`11–12 of ${this.data.length}`, 'it navigates to next page');
|
||
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 row renders');
|
||
// Changing the @data arg should trigger an update and reset pagination
|
||
// We have to use `this.set` to trigger did-update
|
||
this.set('data', [
|
||
{ island: 'Palawan', visit_length: 9, is_booked: true, trip_date: '2025-11-14T00:00:00.000Z' },
|
||
{ island: 'Mykonos', visit_length: 3, is_booked: false, trip_date: '2026-02-28T00:00:00.000Z' },
|
||
]);
|
||
|
||
// There's a workaround using next() from @ember/runloop because the Hds::Pagination::Numbered component
|
||
// doesn't re-render when @currentPage updates. When that's fixed at the source we should be able to remove waitFor
|
||
await waitFor(GENERAL.paginationInfo);
|
||
assert.dom(GENERAL.paginationInfo).hasText(`1–2 of ${this.data.length}`);
|
||
assert.dom(GENERAL.tableRow()).exists({ count: 2 }, '2 rows render');
|
||
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page selector is unchanged when data updates');
|
||
});
|
||
});
|