vault/ui/tests/integration/components/list-table-test.js
Vault Automation b057aac746
[VAULT-43339] 1/2 Chore update TS (#13050) (#13105)
* Initial ts updgrade

* Migrate linked-block to ts to squash ts errors

* [VAULT-43339] 2/2 Update vault-reporting and add ember-intl (#13062)

* Update vault-reporting and add ember-intl

* Add setupIntl for rendering tests

Co-authored-by: Jim Wright <jim.wright@hashicorp.com>
2026-03-17 15:52:40 -07:00

206 lines
9.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, fillIn, render, waitFor } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import sinon from 'sinon';
const MOCK_DATA = [
{ island: 'Maldives', visit_length: 5, trip_date: '2025-06-22T00:00:00.000Z' },
{ island: 'Bora Bora', visit_length: 7, trip_date: '2025-03-15T00:00:00.000Z' },
{ island: 'Fiji', visit_length: 10, trip_date: '2025-09-08T00:00:00.000Z' },
{ island: 'Santorini', visit_length: 4, trip_date: '2026-04-10T00:00:00.000Z' },
{ island: 'Maui', visit_length: 8, trip_date: '2026-01-18T00:00:00.000Z' },
{ island: 'Seychelles', visit_length: 6, trip_date: '2025-12-03T00:00:00.000Z' },
];
module('Integration | Component | list-table', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(async function () {
this.data = MOCK_DATA;
this.onSelectionChange = undefined;
this.selectionKeyField = undefined;
this.columns = [
{ key: 'island', label: 'Islands', isSortable: true },
{ key: 'visit_length', label: 'Visit length', customTableItem: true },
{ key: 'trip_date', label: 'Date trip starts' },
{ key: 'popupMenu', label: 'Action' },
];
this.renderComponent = async () => {
return render(hbs`
<ListTable
@columns={{this.columns}}
@data={{this.data}}
@selectionKeyField={{this.selectionKeyField}}
@onSelectionChange={{this.onSelectionChange}}
>
<:customTableItem as |itemData|>
<Hds::BadgeCount @text={{itemData.visit_length}} @type="outlined" />
</:customTableItem>
<:popupMenu as |rowData|>
<Hds::Dropdown as |D|>
<D.ToggleButton @text="Menu" data-test-popup-menu-trigger />
<D.Title @text={{rowData.island}} />
<D.Description @text="Sample text" />
<D.Interactive @route="components" @icon="trash" @color="critical">Delete</D.Interactive>
</Hds::Dropdown>
</:popupMenu>
</ListTable>`);
};
});
test('it renders and paginates data', async function (assert) {
await this.renderComponent();
assert.dom('input[type="checkbox"]').doesNotExist('table is not selectable by default');
assert.dom(GENERAL.paginationInfo).hasText(`16 of ${this.data.length}`);
// Default is 10, so change to something else to test pagination
await fillIn(GENERAL.paginationSizeSelector, '5');
assert.dom(GENERAL.paginationInfo).hasText(`15 of ${this.data.length}`);
assert.dom(GENERAL.tableRow()).exists({ count: 5 }, 'only 5 rows render');
await click(GENERAL.nextPage);
assert.dom(GENERAL.paginationInfo).hasText(`66 of ${this.data.length}`);
assert.dom(GENERAL.tableRow()).exists({ count: 1 }, 'only 1 row renders on second page');
assert.dom(GENERAL.tableData(0, 'island')).hasText('Seychelles', 'second page has expected row');
});
test('it does not render popup menu if @columns does not include a popupMenu key', async function (assert) {
this.columns = [
{ key: 'island', label: 'Islands', isSortable: true },
{ key: 'visit_length', label: 'Visit length', customTableItem: true },
{ key: 'trip_date', label: 'Date trip starts' },
];
await this.renderComponent();
assert.dom(GENERAL.menuTrigger).doesNotExist();
});
test('it stringifies object and array values for non-custom columns', async function (assert) {
this.columns = [
{ key: 'island', label: 'Islands' },
{ key: 'trip_details', label: 'Trip details' },
{ key: 'tags', label: 'Tags' },
];
this.data = [
{
island: 'Maldives',
trip_details: { hotel: 'Atoll Inn', nights: 5 },
tags: ['beach', 'snorkel'],
},
];
await this.renderComponent();
assert.dom(GENERAL.tableData(0, 'trip_details')).hasText('{ "hotel": "Atoll Inn", "nights": 5 }');
assert.dom(GENERAL.tableData(0, 'tags')).hasText('[ "beach", "snorkel" ]');
});
test('it does not render popup menu if parent does not yield one', async function (assert) {
await render(hbs`
<ListTable
@columns={{this.columns}}
@data={{this.data}}
@selectionKeyField={{this.selectionKeyField}}
@onSelectionChange={{this.onSelectionChange}}
/>`);
assert.dom(GENERAL.menuTrigger).doesNotExist();
});
test('it sorts table data by a sortable column', async function (assert) {
await this.renderComponent();
await click(GENERAL.icon('swap-vertical'));
const expectedOrder = ['Bora Bora', 'Fiji', 'Maldives', 'Maui', 'Santorini', 'Seychelles'];
expectedOrder.forEach((island, idx) => {
assert.dom(GENERAL.tableData(idx, 'island')).hasText(island);
});
});
test('action column renders provided yield block with popup menu', async function (assert) {
await this.renderComponent();
assert.dom(GENERAL.menuTrigger).exists({ count: this.data.length }, 'popup trigger exists for each item');
await click(`${GENERAL.tableRow(2)} ${GENERAL.menuTrigger}`);
assert.dom('li').hasText(this.data[2].island, 'popup menu renders relevant row data');
});
test('selectable checkboxes render and are selectable when selectionKeyField is provided', async function (assert) {
this.selectionKeyField = 'island';
const count = this.data.length + 1;
this.onSelectionChange = sinon.spy();
await this.renderComponent();
assert
.dom('input[type="checkbox"]')
.exists({ count }, 'it renders a checkbox for each row plus the header to select all');
assert
.dom(`${GENERAL.tableRow(0)} input[type="checkbox"]`)
.hasAttribute(
'aria-label',
`Select row ${this.data[0][this.selectionKeyField]}`,
'selection aria label suffix uses selectionKeyField in value'
);
await click(`${GENERAL.tableRow(0)} input[type="checkbox"]`);
await click(`${GENERAL.tableRow(2)} input[type="checkbox"]`);
assert.true(this.onSelectionChange.calledTwice, 'onSelectionChange is called twice');
const [callbackArgs] = this.onSelectionChange.lastCall.args;
const { selectionKey, selectedRowsKeys, selectableRowsStates } = callbackArgs;
const lastItemSelected = this.data[2];
assert.strictEqual(selectionKey, lastItemSelected.island, 'selectionKey is last selected row');
assert.propEqual(selectedRowsKeys, ['Maldives', 'Fiji'], 'callback passes selectedRowKeys');
const expectedRowStates = [
{ selectionKey: 'Maldives', isSelected: true },
{ selectionKey: 'Bora Bora', isSelected: false },
{ selectionKey: 'Fiji', isSelected: true },
{ selectionKey: 'Santorini', isSelected: false },
{ selectionKey: 'Maui', isSelected: false },
{ selectionKey: 'Seychelles', isSelected: false },
];
assert.propEqual(selectableRowsStates, expectedRowStates, 'callback contains selectableRowsStates');
});
test('it is still selectable when selection callback is not provided', async function (assert) {
this.selectionKeyField = 'island';
await this.renderComponent();
assert.dom('input[type="checkbox"]').exists({ count: this.data.length + 1 });
const firstRowCheckbox = '[data-test-table-row="0"] input[type="checkbox"]';
await click(firstRowCheckbox);
assert.dom(firstRowCheckbox).isChecked('row checkbox can be toggled without @onSelectionChange');
});
test('custom item renders provided yield block with customTableItem for a column has customTableItem set to true', async function (assert) {
await this.renderComponent();
assert
.dom(`${GENERAL.tableData(0, 'visit_length')} .hds-badge-count`)
.hasText('5', 'custom table item renders yielded badge');
});
test('it resets pagination when data changes', async function (assert) {
const moreData = [
{ island: 'Tahiti', visit_length: 12, trip_date: '2025-05-10T00:00:00.000Z' },
{ island: 'Barbados', visit_length: 6, trip_date: '2025-08-25T00:00:00.000Z' },
{ island: 'Cyprus', visit_length: 9, trip_date: '2026-03-12T00:00:00.000Z' },
{ island: 'Jamaica', visit_length: 7, trip_date: '2025-11-05T00:00:00.000Z' },
{ island: 'Crete', visit_length: 11, trip_date: '2026-06-18T00:00:00.000Z' },
{ island: 'Aruba', visit_length: 5, trip_date: '2025-10-14T00:00:00.000Z' },
];
this.data = [...MOCK_DATA, ...moreData];
await this.renderComponent();
await click(GENERAL.nextPage);
assert.dom(GENERAL.paginationInfo).hasText(`1112 of ${this.data.length}`, 'it navigates to next page');
// Changing the @data arg should trigger an update and reset pagination
this.set('data', [
{ island: 'Palawan', visit_length: 9, trip_date: '2025-11-14T00:00:00.000Z' },
{ island: 'Mykonos', visit_length: 3, 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(`12 of ${this.data.length}`);
assert.dom(GENERAL.paginationSizeSelector).hasValue('10', 'page selector is unchanged when data updates');
});
});