diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2a145ccb7f..f2523f52165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -499,6 +499,9 @@ importers: '@codemirror/state': specifier: ^6.5.0 version: 6.5.2 + '@codemirror/view': + specifier: ^6.36.2 + version: 6.38.1 '@ember/optional-features': specifier: ^2.2.0 version: 2.2.0 @@ -565,6 +568,9 @@ importers: '@types/rsvp': specifier: ^4.0.9 version: 4.0.9 + '@types/sinon': + specifier: ^17.0.4 + version: 17.0.4 broccoli-asset-rev: specifier: ^3.0.0 version: 3.0.0 @@ -3758,6 +3764,12 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/sinon@17.0.4': + resolution: {integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==} + + '@types/sinonjs__fake-timers@15.0.0': + resolution: {integrity: sha512-lqKG4X0fO3aJF7Bz590vuCkFt/inbDyL7FXaVjPEYO+LogMZ2fwSDUiP7bJvdYHaCgCQGNOPxquzSrrnVH3fGw==} + '@types/sizzle@2.3.9': resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} @@ -15438,6 +15450,12 @@ snapshots: '@types/node': 22.17.0 '@types/send': 0.17.4 + '@types/sinon@17.0.4': + dependencies: + '@types/sinonjs__fake-timers': 15.0.0 + + '@types/sinonjs__fake-timers@15.0.0': {} + '@types/sizzle@2.3.9': {} '@types/stack-utils@2.0.3': {} diff --git a/showcase/package.json b/showcase/package.json index 471363afa66..a11002c09cb 100644 --- a/showcase/package.json +++ b/showcase/package.json @@ -41,6 +41,7 @@ "@babel/plugin-proposal-decorators": "^7.27.1", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.36.2", "@ember/optional-features": "^2.2.0", "@ember/render-modifiers": "^2.1.0", "@ember/string": "^4.0.1", @@ -63,6 +64,7 @@ "@tsconfig/ember": "^3.0.10", "@types/qunit": "^2.19.12", "@types/rsvp": "^4.0.9", + "@types/sinon": "^17.0.4", "broccoli-asset-rev": "^3.0.0", "concurrently": "^9.1.2", "ember-a11y-testing": "^7.1.2", @@ -131,5 +133,9 @@ }, "ember": { "edition": "octane" + }, + "exports": { + "./tests/*": "./tests/*", + "./*": "./app/*" } } diff --git a/showcase/tests/integration/components/hds/accordion/index-test.js b/showcase/tests/integration/components/hds/accordion/index-test.gts similarity index 61% rename from showcase/tests/integration/components/hds/accordion/index-test.js rename to showcase/tests/integration/components/hds/accordion/index-test.gts index 4ff330ab5ac..1a90963a0ba 100644 --- a/showcase/tests/integration/components/hds/accordion/index-test.js +++ b/showcase/tests/integration/components/hds/accordion/index-test.gts @@ -4,71 +4,87 @@ */ import { module, test } from 'qunit'; +import { click, render, settled, find } from '@ember/test-helpers'; +import { on } from '@ember/modifier'; +import { TrackedObject } from 'tracked-built-ins'; + +import { + HdsAccordion, + HdsAccordionItem, +} from '@hashicorp/design-system-components/components'; +import type { HdsAccordionForceStates } from '@hashicorp/design-system-components/components/hds/accordion/types'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { click, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/accordion/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-accordion').hasClass('hds-accordion'); }); // CONTENT test('it renders the passed in Accordion Items', async function (assert) { - await render(hbs` - - - <:toggle>Item one - <:content>Content one - - - <:toggle>Item two - <:content>Content two - - - `); + await render( + , + ); assert.dom('.hds-accordion .hds-accordion-item').exists({ count: 2 }); }); test('it renders the passed in content in the Accordion Item', async function (assert) { - await render(hbs` - - - <:toggle>Item one - <:content>Content one - - - `); + await render( + , + ); await click('.hds-accordion-item__button'); assert.dom('#test-strong').exists().hasText('Item one'); assert.dom('#test-em').exists().hasText('Content one'); }); test('it renders a div when the @titleTag argument is not provided', async function (assert) { - await render(hbs` - - - <:toggle>Item one - <:content>Content one - - - `); + await render( + , + ); assert.dom('.hds-accordion-item__toggle-content').hasTagName('div'); }); test('it renders the custom title tag when the @titleTag argument is provided', async function (assert) { - await render(hbs` - - - <:toggle>Item one - <:content>Content one - - - `); + await render( + , + ); assert.dom('.hds-accordion-item__toggle-content').hasTagName('h2'); }); @@ -76,11 +92,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render the medium size as the default if no @size is declared', async function (assert) { await render( - hbs` - - Item - - `, + , ); assert.dom('#test-accordion').hasClass('hds-accordion--size-medium'); assert @@ -90,11 +109,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render the correct CSS size class depending on the @size', async function (assert) { await render( - hbs` - - Item - - `, + , ); assert.dom('#test-accordion').hasClass('hds-accordion--size-large'); assert @@ -104,12 +126,18 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render different CSS size classes when different @size arguments are provided', async function (assert) { await render( - hbs` - - Item 1 - Item 2 - - `, + , ); assert .dom('#test-accordion-item1') @@ -123,11 +151,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render the card type as the default if no @type is declared', async function (assert) { await render( - hbs` - - Item - - `, + , ); assert.dom('#test-accordion').hasClass('hds-accordion--type-card'); assert @@ -137,11 +168,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render the correct CSS type class depending on the @type', async function (assert) { await render( - hbs` - - Item - - `, + , ); assert.dom('#test-accordion').hasClass('hds-accordion--type-flush'); assert @@ -151,12 +185,18 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it should render different CSS type class when different @type arguments are provided', async function (assert) { await render( - hbs` - - Item 1 - Item 2 - - `, + , ); assert .dom('#test-accordion-item1') @@ -170,14 +210,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it displays the correct value for aria-expanded on the AccordionItem when closed vs open', async function (assert) { await render( - hbs` - + , ); assert .dom('.hds-accordion-item__button') @@ -190,38 +230,37 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('the AccordionItem toggle button has an aria-controls attribute with a value matching the DisclosurePrimitive content id', async function (assert) { await render( - hbs` - + , ); await click('.hds-accordion-item__button'); assert.dom('.hds-accordion-item__button').hasAttribute('aria-controls'); + const accordionButton = find('.hds-accordion-item__button'); + const accordionContent = find('.hds-disclosure-primitive__content'); + assert.strictEqual( - this.element - .querySelector('.hds-accordion-item__button') - .getAttribute('aria-controls'), - this.element - .querySelector('.hds-disclosure-primitive__content') - .getAttribute('id'), + accordionButton?.getAttribute('aria-controls'), + accordionContent?.getAttribute('id'), ); }); test('the AccordionItem toggle has an aria-labelledby attribute set to the id of the toggle text by default', async function (assert) { await render( - hbs` - + , ); assert.dom('.hds-accordion-item__button').hasAttribute('aria-labelledby'); @@ -230,26 +269,25 @@ module('Integration | Component | hds/accordion/index', function (hooks) { .dom('.hds-accordion-item__button') .doesNotHaveAttribute('aria-label'); + const accordionButton = find('.hds-accordion-item__button'); + const accordionToggleContent = find('.hds-accordion-item__toggle-content'); + assert.strictEqual( - this.element - .querySelector('.hds-accordion-item__toggle-content') - .getAttribute('id'), - this.element - .querySelector('.hds-accordion-item__button') - .getAttribute('aria-labelledby'), + accordionToggleContent?.getAttribute('id'), + accordionButton?.getAttribute('aria-labelledby'), ); }); test('the AccordionItem toggle has an aria-label attribute when the argument is passed', async function (assert) { await render( - hbs` - + , ); assert @@ -267,14 +305,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { test('it displays content initially when @isOpen is set to true', async function (assert) { await render( - hbs` - + , ); // Test content is displayed assert @@ -289,18 +327,21 @@ module('Integration | Component | hds/accordion/index', function (hooks) { // containsInteractive test('it displays the correct variant when containsInteractive is set to false vs. true', async function (assert) { await render( - hbs` - + , ); assert .dom('#test-contains-interactive--false') @@ -313,14 +354,14 @@ module('Integration | Component | hds/accordion/index', function (hooks) { // isStatic test('it does not show the toggle button when @isStatic is set to true, ', async function (assert) { await render( - hbs` - + , ); assert.dom('.hds-accordion-item--is-static').exists(); assert @@ -330,9 +371,15 @@ module('Integration | Component | hds/accordion/index', function (hooks) { // forceState test('it displays the correct content based on @forceState', async function (assert) { + const context = new TrackedObject< + Record<'isOpen', HdsAccordionForceStates> + >({ + isOpen: 'close', + }); + await render( - hbs` - + , ); // first item open at rendering assert @@ -351,7 +398,8 @@ module('Integration | Component | hds/accordion/index', function (hooks) { .containsText('Content one'); // all items open via forceState (external override to open) - this.set('forceState', 'open'); + context.isOpen = 'open'; + await settled(); assert.dom('.hds-accordion-item__content').exists({ count: 2 }); // first item closed via toggle (internal override to close) @@ -362,7 +410,8 @@ module('Integration | Component | hds/accordion/index', function (hooks) { .containsText('Content two'); // all items closed via forceState (external override to close) - this.set('forceState', 'close'); + context.isOpen = 'close'; + await settled(); assert.dom('.hds-accordion-item__content').doesNotExist(); // first item open via toggle (internal override to open) @@ -376,14 +425,16 @@ module('Integration | Component | hds/accordion/index', function (hooks) { // close test('it should hide the content when an accordion item triggers `close`', async function (assert) { - await render(hbs` - - <:toggle>Item one - <:content as |c|> - - - - `); + await render( + , + ); await click('.hds-accordion-item__button'); assert.dom('.hds-accordion-item__content').exists(); @@ -395,26 +446,38 @@ module('Integration | Component | hds/accordion/index', function (hooks) { // onClickToggle test('it should call onClickToggle function', async function (assert) { - let state = 'close'; - this.set( - 'onClickToggle', - () => (state = state === 'open' ? (state = 'close') : (state = 'open')), + const context = new TrackedObject< + Record<'isOpen', HdsAccordionForceStates> + >({ + isOpen: 'close', + }); + + const onClickToggle = () => + (context.isOpen = + context.isOpen === 'open' + ? (context.isOpen = 'close') + : (context.isOpen = 'open')); + + await render( + , ); - await render(hbs` - - <:toggle>Item one - <:content>Content one - - `); // closed by default assert.dom('.hds-accordion-item__content').doesNotExist(); // toggle to open await click('.hds-accordion-item__button'); - assert.strictEqual(state, 'open'); + assert.strictEqual(context.isOpen, 'open'); assert.dom('.hds-accordion-item__content').exists(); // toggle to close await click('.hds-accordion-item__button'); - assert.strictEqual(state, 'close'); + assert.strictEqual(context.isOpen, 'close'); assert.dom('.hds-accordion-item__content').doesNotExist(); }); }); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/column-reordering-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/column-reordering-test.gts new file mode 100644 index 00000000000..d08d2dc87b3 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/column-reordering-test.gts @@ -0,0 +1,757 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { get } from '@ember/helper'; +import { + click, + find, + findAll, + focus, + render, + settled, + setupOnerror, + triggerEvent, + triggerKeyEvent, + // pauseTest, +} from '@ember/test-helpers'; +import { TrackedObject, TrackedArray } from 'tracked-built-ins'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { HdsAdvancedTableColumnReorderSide } from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +const getColumnByLabel = ( + columns: typeof DEFAULT_REORDERABLE_COLUMNS, + label: string, +) => { + return columns.find((col) => col.label === label); +}; + +const getColumnOrder = () => { + const thElements = findAll('.hds-advanced-table__th'); + + return thElements.map((th) => { + const column = getColumnByLabel( + DEFAULT_REORDERABLE_COLUMNS, + th.textContent.trim(), + ); + + return column ? column.key : undefined; + }); +}; + +const startReorderDrag = async (handleElement: Element | null) => { + if (!handleElement) return; + + console.log('handle element exists'); + return triggerEvent(handleElement, 'dragstart'); +}; + +const getTargetElementFromColumnIndex = (index: number) => { + const dropTargets = findAll('.hds-advanced-table__th-reorder-drop-target'); + const target = dropTargets[index]; + + if (target === null) { + throw new Error( + `Target column at index ${index} not found after drag started.`, + ); + } + + return target; +}; + +const getDragTargetPosition = ( + targetElement: Element, + targetPosition: HdsAdvancedTableColumnReorderSide, +) => { + const targetRect = targetElement.getBoundingClientRect(); + let clientX; + + switch (targetPosition) { + case 'left': + clientX = targetRect.left + 1; + break; + default: + clientX = targetRect.right - 1; + } + + return { clientX, clientY: targetRect.top + targetRect.height / 2 }; +}; + +const dragOverTarget = async ( + target: Element, + { clientX, clientY }: { clientX: number; clientY: number }, +) => { + await triggerEvent(target, 'dragenter', { clientX, clientY }); + await triggerEvent(target, 'dragover', { clientX, clientY }); +}; + +const simulateColumnReorderDrag = async ({ + handleElement, + targetElement, + targetIndex, + targetPosition = 'left', +}: { + handleElement?: Element | null; + targetElement?: Element | null; + targetIndex: number; + targetPosition?: HdsAdvancedTableColumnReorderSide; +}): Promise<{ + target?: Element | null; + eventOptions: { clientX: number; clientY: number }; +}> => { + console.log(handleElement, 'handle element passed in'); + if (!handleElement || !targetElement) { + return Promise.resolve({ + target: null, + eventOptions: { clientX: 0, clientY: 0 }, + }); + } + + console.log(handleElement, 'handle element found'); + + await startReorderDrag(handleElement); + + // await pauseTest(); + + const target = targetElement ?? getTargetElementFromColumnIndex(targetIndex); + + const { clientX, clientY } = getDragTargetPosition(target, targetPosition); + const eventOptions = { clientX, clientY }; + await dragOverTarget(target, eventOptions); + // return the target event options for further use, if needed + return { target, eventOptions }; +}; + +const simulateColumnReorderDrop = async ({ + target, + handleElement, + eventOptions, +}: { + target?: Element | null; + handleElement?: Element | null; + eventOptions: Record; +}) => { + if (!target || !handleElement) { + return; + } + + await triggerEvent(target, 'drop', eventOptions); + await triggerEvent(handleElement, 'dragend'); +}; + +const DEFAULT_REORDERABLE_COLUMNS = [ + { key: 'artist', label: 'Artist' }, + { key: 'album', label: 'Album' }, + { key: 'year', label: 'Year' }, +]; + +const DEFAULT_REORDERABLE_MODEL = [ + { id: '1', artist: 'Nick Drake', album: 'Pink Moon', year: '1972' }, + { id: '2', artist: 'The Beatles', album: 'Abbey Road', year: '1969' }, + { id: '3', artist: 'Melanie', album: 'Candles in the Rain', year: '1971' }, +]; + +const createReorderableTable = async (options: { + columnOrder?: string[]; + hasStickyFirstColumn?: boolean; +}) => { + await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('column reordering', function () { + test('it renders reorder handles when reordering is enabled', async function (assert) { + const context = new TrackedObject({ + hasReorderableColumns: false, + }); + + await render( + , + ); + + assert + .dom('.hds-advanced-table__th-reorder-handle') + .doesNotExist( + 'No reorder handles are rendered when reordering is disabled', + ); + + context.hasReorderableColumns = true; + await settled(); + + assert + .dom('.hds-advanced-table__th-reorder-handle') + .exists({ count: 3 }, 'All columns have a reorder handle'); + }); + + test('it does not render a reorder handle on the row selection column', async function (assert) { + await createReorderableTable({}); + + const selectAllThSelector = + '[role="columnheader"].hds-advanced-table__th--is-selectable'; + const reorderHandleSelector = '.hds-advanced-table__th-reorder-handle'; + + assert + .dom(`${selectAllThSelector} ${reorderHandleSelector}`) + .doesNotExist( + 'No reorder handle is rendered on the row selection column', + ); + }); + + test('columns can be reordered by dragging and dropping', async function (assert) { + await createReorderableTable({}); + + let columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + DEFAULT_REORDERABLE_COLUMNS.map((col) => col.key), + 'Initial column order is correct', + ); + + const expectedDropTargetIndex = 2; + const expectedDropTargetDropSide = 'right'; + + // get the first reorder handle + const reorderHandle = find('.hds-advanced-table__th-reorder-handle'); + + // drag to the right side of the last column + const { target, eventOptions } = await simulateColumnReorderDrag({ + handleElement: reorderHandle, + targetIndex: expectedDropTargetIndex, + targetPosition: expectedDropTargetDropSide, + }); + + // get all drop targets for test reference + const dropTargets = findAll( + '.hds-advanced-table__th-reorder-drop-target', + ); + const originDropTarget = dropTargets[0]; + const destinationDropTarget = dropTargets[expectedDropTargetIndex]; + + assert + .dom(originDropTarget) + .hasClass( + 'hds-advanced-table__th-reorder-drop-target--is-being-dragged', + 'First column is being dragged', + ); + assert + .dom(destinationDropTarget) + .hasClass( + 'hds-advanced-table__th-reorder-drop-target--is-dragging-over', + ) + .hasClass( + `hds-advanced-table__th-reorder-drop-target--is-dragging-over--${expectedDropTargetDropSide}`, + ); + + await simulateColumnReorderDrop({ + target, + handleElement: reorderHandle, + eventOptions, + }); + + columnOrder = getColumnOrder(); + + assert + .dom('.hds-advanced-table__th-reorder-drop-target') + .doesNotExist('Drop targets are removed after drop'); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + ], + 'Columns are reordered correctly after drag and drop', + ); + }); + + test('dropping a target on the nearest side of the next sibling does not reorder columns', async function (assert) { + await createReorderableTable({}); + + const initialColumnOrder = DEFAULT_REORDERABLE_COLUMNS.map( + (col) => col.key, + ); + + let columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + initialColumnOrder, + 'Initial column order is correct', + ); + + const reorderHandle = find('.hds-advanced-table__th-reorder-handle'); + + const { target, eventOptions } = await simulateColumnReorderDrag({ + handleElement: reorderHandle, + targetIndex: 1, + targetPosition: 'left', + }); + + const dropTargets = findAll( + '.hds-advanced-table__th-reorder-drop-target', + ); + const originDropTarget = dropTargets[0]; + const destinationDropTarget = dropTargets[1]; + + // await pauseTest(); + + assert + .dom(originDropTarget) + .hasClass( + 'hds-advanced-table__th-reorder-drop-target--is-being-dragged', + 'First column is being dragged', + ); + assert + .dom(destinationDropTarget) + .doesNotHaveClass( + 'hds-advanced-table__th-reorder-drop-target--is-dragging-over', + ) + .doesNotHaveClass( + 'hds-advanced-table__th-reorder-drop-target--is-dragging-over--left', + ); + + await simulateColumnReorderDrop({ + target, + handleElement: reorderHandle, + eventOptions, + }); + + columnOrder = getColumnOrder(); + + assert + .dom('.hds-advanced-table__th-reorder-drop-target') + .doesNotExist('Drop targets are removed after drop'); + assert.deepEqual( + columnOrder, + initialColumnOrder, + 'Columns order is unchanged after drop on the nearest side', + ); + }); + + test('it should show the context menu with the correct options when reordering is enabled', async function (assert) { + await createReorderableTable({}); + + const thElements = findAll('.hds-advanced-table__th'); // find all header cells + + assert.ok( + thElements[0]?.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const firstContextMenuToggle = thElements[0]?.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (firstContextMenuToggle) { + await click(firstContextMenuToggle); + assert.dom('[data-test-context-option-key="reorder-column"]').exists(); + assert + .dom('[data-test-context-option-key="move-column-to-start"]') + .doesNotExist(); + assert + .dom('[data-test-context-option-key="move-column-to-end"]') + .exists(); + } + + const secondContextMenuToggle = thElements[1]?.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (secondContextMenuToggle) { + await click(secondContextMenuToggle); + assert.dom('[data-test-context-option-key="reorder-column"]').exists(); + assert + .dom('[data-test-context-option-key="move-column-to-start"]') + .exists(); + assert + .dom('[data-test-context-option-key="move-column-to-end"]') + .exists(); + } + + const lastContextMenuToggle = thElements[ + thElements.length - 1 + ]?.querySelector('.hds-dropdown-toggle-icon'); + + if (lastContextMenuToggle) { + await click(lastContextMenuToggle); + assert.dom('[data-test-context-option-key="reorder-column"]').exists(); + assert + .dom('[data-test-context-option-key="move-column-to-start"]') + .exists(); + assert + .dom('[data-test-context-option-key="move-column-to-end"]') + .doesNotExist(); + } + }); + + test('clicking the "Move column" context menu option focuses the reorder handle', async function (assert) { + await createReorderableTable({}); + + const thElements = findAll('.hds-advanced-table__th'); + + const firstContextMenuToggle = thElements[0]?.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (firstContextMenuToggle) { + await click(firstContextMenuToggle); + await click('[data-test-context-option-key="reorder-column"]'); + + const firstReorderHandle = thElements[0]?.querySelector( + '.hds-advanced-table__th-reorder-handle', + ); + + assert.dom(firstReorderHandle).isFocused(); + } + }); + + test('clicking the "Move column to start" context menu option moves the column to the start', async function (assert) { + await createReorderableTable({}); + + const thElements = findAll('.hds-advanced-table__th'); + + const secondContextMenuToggle = thElements[1]?.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (secondContextMenuToggle) { + await click(secondContextMenuToggle); + await click('[data-test-context-option-key="move-column-to-start"]'); + + const columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + ], + 'The second column is moved to the start', + ); + } + }); + + test('clicking the "Move column to end" context menu option moves the column to the end', async function (assert) { + await createReorderableTable({}); + + const thElements = findAll('.hds-advanced-table__th'); + + if (thElements[1]) { + const secondContextMenuToggle = thElements[1].querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (secondContextMenuToggle) { + await click(secondContextMenuToggle); + await click('[data-test-context-option-key="move-column-to-end"]'); + + const columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + ], + 'The second column is moved to the end', + ); + } + } + }); + + test('pressing "Left Arrow" and "Right Arrow" keys when the reorder handle is focused moves the column', async function (assert) { + await createReorderableTable({}); + + const thElements = findAll('.hds-advanced-table__th'); + const firstThElement = thElements[0]; + const firstReorderHandle = thElements[0]?.querySelector( + '.hds-advanced-table__th-reorder-handle', + ); + + if (firstReorderHandle && firstThElement) { + await focus(firstThElement); + await focus(firstReorderHandle); + assert.dom(firstReorderHandle).isFocused(); + + await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowRight'); + let columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + ], + 'The first column is moved to the right', + ); + assert.dom(firstReorderHandle).isFocused(); + + await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowRight'); + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + ], + 'The second column is moved to the right', + ); + assert.dom(firstReorderHandle).isFocused(); + + await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowLeft'); + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + [ + DEFAULT_REORDERABLE_COLUMNS[1]?.key, + DEFAULT_REORDERABLE_COLUMNS[0]?.key, + DEFAULT_REORDERABLE_COLUMNS[2]?.key, + ], + 'The third column is moved back to the left', + ); + assert.dom(firstReorderHandle).isFocused(); + } + }); + + test('passing in columnOrder sets the initial order of the table columns', async function (assert) { + await createReorderableTable({ + columnOrder: ['album', 'year', 'artist'], + }); + + const columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['album', 'year', 'artist'], + 'The initial column order is set correctly', + ); + }); + + test('updating columnOrder externally changes the order of the table columns', async function (assert) { + const context = new TrackedObject({ + columnOrder: ['album', 'year', 'artist'], + }); + + await render( + , + ); + + let columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['album', 'year', 'artist'], + 'The initial column order is set correctly', + ); + + context.columnOrder = ['year', 'album', 'artist']; + await settled(); + + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['year', 'album', 'artist'], + 'The column order is updated correctly', + ); + }); + + test('it throws an assertion if @hasStickyFirstColumn is true and @hasReorderableColumns is true', async function (assert) { + const errorMessage = + 'Cannot have both reorderable columns and a sticky first column.'; + + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createReorderableTable({ + hasStickyFirstColumn: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('column reordering works when there columns are added and removed dynamically', async function (assert) { + const artistColumn = { key: 'artist', label: 'Artist' }; + const albumColumn = { key: 'album', label: 'Album' }; + const yearColumn = { key: 'year', label: 'Year' }; + const genreColumn = { key: 'genre', label: 'Genre' }; + + const availableColumns = [ + artistColumn, + albumColumn, + yearColumn, + genreColumn, + ]; + + // when dealing with dynamic columns, you must handle the order of all potential columns rather than just the ones currently rendered + // inital column order is 'artist', 'album', 'year', 'genre' + const initialColumnOrder = availableColumns.map((col) => col.key); + + // initially set the columns in the reverse order to ensure the table respects the column order and ommit the genre column + const initialColumns = availableColumns + .filter((col) => col.key !== 'genre') + .reverse(); + + const context = new TrackedObject({ + columns: initialColumns, + model: new TrackedArray( + DEFAULT_REORDERABLE_MODEL.map((item) => ({ + ...item, + genre: 'music', + })), + ), + columnOrder: new TrackedArray(initialColumnOrder), + }); + + await render( + , + ); + + // make sure the initial column order is correct based on the columnOrder + let columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['artist', 'album', 'year'], + 'The initial column order is set correctly', + ); + + // add the genre column and ensure it is in the correct order based on columnOrder + context.columns = [genreColumn, ...context.columns]; + await settled(); + + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['artist', 'album', 'year', 'genre'], + 'The column is added in the correct order based on columnOrder', + ); + + // will drop the column to the right side of the third column (year) + const expectedDropTargetIndex = 2; + const expectedDropTargetDropSide = 'right'; + + // get the first reorder handle + const firstReorderHandle = findAll( + '.hds-advanced-table__th-reorder-handle', + )[0]; + + // drag to the right side of the third column (year) + const { target, eventOptions } = await simulateColumnReorderDrag({ + handleElement: firstReorderHandle, + targetIndex: expectedDropTargetIndex, + targetPosition: expectedDropTargetDropSide, + }); + + // drop the column + await simulateColumnReorderDrop({ + target, + handleElement: firstReorderHandle, + eventOptions, + }); + + // column order updates correctly after the drag and drop + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['album', 'year', 'artist', 'genre'], + 'The initial column order is set correctly', + ); + + // remove the year column and ensure the column order is still correct + context.columns = context.columns.filter((col) => col.key !== 'year'); + await settled(); + + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['album', 'artist', 'genre'], // album, year (hidden), artist, genre + 'The column order is correct after a column is removed', + ); + + // move the album column to the end + const albumReorderHandle = findAll( + '.hds-advanced-table__th-reorder-handle', + )[0]; + const lastIndex = context.columns.length - 1; + + const dragResult = await simulateColumnReorderDrag({ + handleElement: albumReorderHandle, + targetIndex: lastIndex, + targetPosition: 'right', + }); + + await simulateColumnReorderDrop({ + ...dragResult, + handleElement: albumReorderHandle, + }); + + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['artist', 'genre', 'album'], // year (hidden), artist, genre, album + 'The column order is correct after another column is moved', + ); + + // add the year column back and ensure it is in the correct position based on columnOrder + context.columns = [...context.columns, yearColumn]; + await settled(); + + columnOrder = getColumnOrder(); + assert.deepEqual( + columnOrder, + ['year', 'artist', 'genre', 'album'], // year, artist, genre, album + 'The column is added back in the correct order based on columnOrder', + ); + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/column-resizing-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/column-resizing-test.gts new file mode 100644 index 00000000000..1e7639dde01 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/column-resizing-test.gts @@ -0,0 +1,520 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { array, hash, get } from '@ember/helper'; +import { + click, + find, + focus, + render, + settled, + triggerEvent, + triggerKeyEvent, +} from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; +import sinon from 'sinon'; +import style from 'ember-style-modifier'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { HdsAdvancedTableColumn } from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +function gridValuesAreEqual( + newGridValues: string[], + originalGridValues: string[], +) { + return newGridValues.every((newGridValue, index) => { + const newGridValueInt = parseInt(newGridValue, 10); + + if (!originalGridValues[index]) { + return false; + } + + const originalGridValueInt = parseInt(originalGridValues[index], 10); + + // Allow for small pixel differences due to CSS grid subpixel rendering in different environments + return Math.abs(newGridValueInt - originalGridValueInt) <= 1; + }); +} + +function getTableGridValues(tableElement: Element | null) { + if (!tableElement) { + return []; + } + + const computedStyle = window.getComputedStyle(tableElement); + const gridTemplateColumns = computedStyle.getPropertyValue( + 'grid-template-columns', + ); + const gridValues = gridTemplateColumns + .split(' ') + .map((value) => value.trim()); + + return gridValues; +} + +async function performContextMenuAction(th: Element | null, key: string) { + const contextMenuToggle = th?.querySelector('.hds-dropdown-toggle-icon'); + + if (contextMenuToggle) { + await click(contextMenuToggle); + return click(`[data-test-context-option-key="${key}"]`); + } +} + +async function simulateRightPointerDrag(handle: Element | null) { + if (!handle) return; + + await triggerEvent(handle, 'pointerdown', { clientX: 100, button: 0 }); + await triggerEvent(handle, 'pointermove', { clientX: 130, buttons: 1 }); + await triggerEvent(window, 'pointerup', { button: 0 }); +} + +const DEFAULT_RESIZABLE_COLUMNS: HdsAdvancedTableColumn[] = [ + { + key: 'col1', + label: 'Col 1', + width: '120px', + minWidth: '60px', + maxWidth: '300px', + }, + { + key: 'col2', + label: 'Col 2', + }, +]; + +const DEFAULT_RESIZABLE_MODEL = [ + { id: '1', col1: 'A', col2: 'B' }, + { id: '2', col1: 'C', col2: 'D' }, +]; + +const createResizableTable = async (options: { + onColumnResize?: (key: string) => void; +}) => { + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('column resizing', function () { + test('it should allow resizing columns with the resize handle (pointer events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + assert + .dom('.hds-advanced-table__th-resize-handle') + .exists( + { count: 1 }, + 'There is one resize handle (not on last column)', + ); + + const handle = find('.hds-advanced-table__th-resize-handle'); // get the first handle + + // Simulate pointer drag to the right (increase width) + await simulateRightPointerDrag(handle); + + const newGridValues = getTableGridValues(table); + assert.notEqual( + newGridValues, + originalGridValues, + 'Grid values changed after drag', + ); + }); + + test('it should allow resizing columns with the resize handle (keyboard events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + // Focus and send ArrowRight key + await focus(handle); + await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); + + let newGridValues = getTableGridValues(table); + + assert.notOk( + gridValuesAreEqual(originalGridValues, newGridValues), + 'Grid values are not equal after ArrowRight', + ); + + // Send ArrowLeft key + await triggerKeyEvent(handle, 'keydown', 'ArrowLeft'); + + newGridValues = getTableGridValues(table); + + assert.ok( + gridValuesAreEqual(originalGridValues, newGridValues), + 'Grid values are equal after ArrowLeft', + ); + } + }); + + test('it should not allow resizing columns below their minimum width (pointer events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + // Try to resize column to a very small width (well below minWidth of 60px) + await triggerEvent(handle, 'pointerdown', { clientX: 100 }); + await triggerEvent(window, 'pointermove', { clientX: 1 }); + await triggerEvent(window, 'pointerup'); + + const newGridValues = getTableGridValues(table); + assert.notEqual( + newGridValues, + originalGridValues, + 'Grid values changed after pointer drag', + ); + + const firstColumnGridValue = newGridValues[0]; + + if (firstColumnGridValue) { + assert.ok( + parseInt(firstColumnGridValue, 10) >= 60, + `Column width respects minimum width constraint (actual: ${firstColumnGridValue}, min: 60px)`, + ); + } + } + }); + + test('it should not allow resizing columns above their maximum width (pointer events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + // Try to resize column to a very large width (well below minWidth of 60px) + await triggerEvent(handle, 'pointerdown', { clientX: 100 }); + await triggerEvent(window, 'pointermove', { clientX: 10000 }); + await triggerEvent(window, 'pointerup'); + + // Check the new width + const newGridValues = getTableGridValues(table); + assert.notEqual( + newGridValues, + originalGridValues, + 'Grid values changed after pointer drag', + ); + + const firstColumnGridValue = newGridValues[0]; + + if (firstColumnGridValue) { + assert.ok( + parseInt(firstColumnGridValue, 10) <= 300, + `Column width respects maximum width constraint (actual: ${firstColumnGridValue}px, max: 300px)`, + ); + } + } + }); + + test('it should not allow resizing columns below their minimum width (keyboard events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + // Focus handle and press ArrowLeft multiple times to try going below min width + await focus(handle); + + for (let i = 0; i < 10; i++) { + // moves left 10px each time + await triggerKeyEvent(handle, 'keydown', 'ArrowLeft'); + } + + const newGridValues = getTableGridValues(table); + assert.notEqual( + newGridValues, + originalGridValues, + 'Grid values changed after ArrowLeft', + ); + + const firstColumnGridValue = newGridValues[0]; + + if (firstColumnGridValue) { + assert.ok( + parseInt(firstColumnGridValue, 10) >= 60, + `Column width respects minimum width constraint with keyboard events (actual: ${firstColumnGridValue}, min: 60px)`, + ); + } + } + }); + + test('it should not allow resizing columns above their maximum width (keyboard events)', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + // Focus handle and press ArrowLeft multiple times to try going below min width + await focus(handle); + + for (let i = 0; i < 10; i++) { + // moves right 10px each time + await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); + } + + const newGridValues = getTableGridValues(table); + assert.notEqual( + newGridValues, + originalGridValues, + 'Grid values changed after ArrowRight', + ); + + const firstColumnGridValue = newGridValues[0]; + + if (firstColumnGridValue) { + assert.ok( + parseInt(firstColumnGridValue, 10) <= 300, + `Column width respects maximum width constraint with keyboard events (actual: ${firstColumnGridValue}px, max: 300px)`, + ); + } + } + }); + + test('it should show the context menu when resizing is enabled', async function (assert) { + await createResizableTable({}); + + const th = find('.hds-advanced-table__th'); // find the first header cell + + if (th) { + assert.ok( + th.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); + + if (contextMenuToggle) { + await click(contextMenuToggle); + + assert + .dom('[data-test-context-option-key="reset-column-width"]') + .exists(); + } + } + }); + + test('it should resize the column to the initial width when resetting column width', async function (assert) { + await createResizableTable({}); + + const table = find('.hds-advanced-table'); + const originalGridValues = getTableGridValues(table); + + const handle = find('.hds-advanced-table__th-resize-handle'); + const th = handle?.closest('.hds-advanced-table__th'); + + await simulateRightPointerDrag(handle); + + let newGridValues = getTableGridValues(table); + + assert.notOk( + gridValuesAreEqual(originalGridValues, newGridValues), + 'Grid values are not equal after resizing', + ); + + if (th) { + await performContextMenuAction(th, 'reset-column-width'); + + newGridValues = getTableGridValues(table); + assert.ok( + gridValuesAreEqual(originalGridValues, newGridValues), + 'Grid values reset to initial state after resetting column width', + ); + } + }); + + test('it should focus the resize handle when the "resize column" context menu option is clicked', async function (assert) { + await createResizableTable({}); + + const handle = find('.hds-advanced-table__th-resize-handle'); + const th = handle?.closest('.hds-advanced-table__th'); + + if (th) { + await performContextMenuAction(th, 'resize-column'); + assert.ok( + handle === document.activeElement, + 'Resize handle is focused', + ); + } + }); + + test('it should call `onColumnResize` when a column is resized by dragging', async function (assert) { + const onColumnResizeSpy = sinon.spy(); + await createResizableTable({ + onColumnResize: onColumnResizeSpy, + }); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + if (handle) { + await focus(handle); + await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); + + assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); + } + }); + + test('it should call `onColumnResize` when a column is resized by keyboard', async function (assert) { + const onColumnResizeSpy = sinon.spy(); + await createResizableTable({ + onColumnResize: onColumnResizeSpy, + }); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + // Simulate pointer drag to the right (increase width) + await simulateRightPointerDrag(handle); + + assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); + }); + + test('it should call `onColumnResize` when a column width is reset', async function (assert) { + const onColumnResizeSpy = sinon.spy((key) => { + console.log('Column resized', key); + }); + + await createResizableTable({ + onColumnResize: onColumnResizeSpy, + }); + + const handle = find('.hds-advanced-table__th-resize-handle'); + + await simulateRightPointerDrag(handle); + + assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); + + if (handle) { + await performContextMenuAction( + handle.closest('.hds-advanced-table__th'), + 'reset-column-width', + ); + assert.ok( + onColumnResizeSpy.calledTwice, + 'onColumnResize was called again after resetting column width', + ); + } + }); + + // Resize behavior tests + test('columns will grow to fill available space when width is not explicitly set', async function (assert) { + const context = new TrackedObject({ + width: '300px', + }); + + await render( + , + ); + + const table = find('#data-test-advanced-table'); + const container = find('#resize-test-container'); + + if (table && container) { + const tableAsHTMLElement = table as HTMLElement; + const containerAsHTMLElement = container as HTMLElement; + + assert.ok( + tableAsHTMLElement.offsetWidth >= containerAsHTMLElement.offsetWidth, + 'Table width is greater than the container width', + ); + + context.width = '100%'; + await settled(); + + assert.ok( + tableAsHTMLElement.offsetWidth === containerAsHTMLElement.offsetWidth, + 'Table width grows to fit container width', + ); + } + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/multi-selection-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/multi-selection-test.gts new file mode 100644 index 00000000000..f67af26b7a1 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/multi-selection-test.gts @@ -0,0 +1,196 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { click, findAll, render } from '@ember/test-helpers'; +import { get } from '@ember/helper'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { HdsAdvancedTableOnSelectionChangeSignature } from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +const DEFAULT_SELECTABLE_MODEL = [ + { + id: '1', + type: 'folk', + artist: 'Nick Drake', + album: 'Pink Moon', + year: '1972', + }, + { + id: '2', + type: 'folk', + artist: 'The Beatles', + album: 'Abbey Road', + year: '1969', + }, + { + id: '3', + type: 'folk', + artist: 'Melanie', + album: 'Candles in the Rain', + year: '1971', + }, +]; + +const DEFAULT_SELECTABLE_COLUMNS = [ + { key: 'artist', label: 'Artist' }, + { key: 'album', label: 'Album' }, + { key: 'year', label: 'Year' }, +]; + +const createSelectableTable = async (options: { + selectionAriaLabelSuffix?: string; + hasStickyFirstColumn?: boolean; + onSelectionChange?: ( + args: HdsAdvancedTableOnSelectionChangeSignature, + ) => void; +}) => { + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('multi-selection', function () { + const selectAllCheckboxSelector = + '#data-test-selectable-advanced-table .hds-advanced-table__thead .hds-advanced-table__th[role="columnheader"] .hds-advanced-table__checkbox'; + const rowCheckboxesSelector = + '#data-test-selectable-advanced-table .hds-advanced-table__tbody .hds-advanced-table__th .hds-advanced-table__checkbox'; + + test('it renders a multi-select table when isSelectable is set to true for a table with a model', async function (assert) { + await createSelectableTable({}); + + assert.dom(selectAllCheckboxSelector).exists({ count: 1 }); + assert + .dom(rowCheckboxesSelector) + .exists({ count: DEFAULT_SELECTABLE_MODEL.length }); + }); + + test('it selects all rows when the "select all" checkbox checked state is triggered', async function (assert) { + await createSelectableTable({}); + + // Default should be unchecked: + assert.dom(selectAllCheckboxSelector).isNotChecked(); + assert.dom(rowCheckboxesSelector).isNotChecked().exists({ count: 3 }); + // Should change to checked after it is triggered: + await click(selectAllCheckboxSelector); + assert.dom(selectAllCheckboxSelector).isChecked(); + assert.dom(rowCheckboxesSelector).isChecked().exists({ count: 3 }); + }); + + test('it deselects all rows when the "select all" checkbox unchecked state is triggered', async function (assert) { + await createSelectableTable({}); + // Trigger checked status: + await click(selectAllCheckboxSelector); + // Trigger unchecked state: + await click(selectAllCheckboxSelector); + assert.dom(selectAllCheckboxSelector).isNotChecked(); + assert.dom(rowCheckboxesSelector).isNotChecked().exists({ count: 3 }); + }); + + test('if some rows are selected but not all, the "select all" checkbox should be in an indeterminate state', async function (assert) { + await createSelectableTable({}); + const rowCheckboxes = findAll(rowCheckboxesSelector); + const firstRowCheckbox = rowCheckboxes[0]; + + if (firstRowCheckbox) { + // Check checkbox in just the first row: + await click(firstRowCheckbox); + assert + .dom(selectAllCheckboxSelector) + .hasProperty('indeterminate', true); + } + }); + + test('it should invoke the `onSelectionChange` callback when a checkbox is selected', async function (assert) { + const context = new TrackedObject<{ + keys: string[]; + }>({ + keys: [], + }); + + const onSelectionChange = ({ + selectedRowsKeys, + }: { + selectedRowsKeys: string[]; + }) => { + context.keys = selectedRowsKeys; + }; + + await createSelectableTable({ onSelectionChange }); + + const rowCheckboxes = findAll(rowCheckboxesSelector); + const firstRowCheckbox = rowCheckboxes[0]; + + if (firstRowCheckbox) { + await click(firstRowCheckbox); + assert.deepEqual(context.keys, ['1']); + } + + await click(selectAllCheckboxSelector); + assert.deepEqual(context.keys, ['1', '2', '3']); + await click(selectAllCheckboxSelector); + assert.deepEqual(context.keys, []); + }); + + test('it renders the expected `aria-label` values for "select all" and rows by default', async function (assert) { + await createSelectableTable({}); + + assert.dom(selectAllCheckboxSelector).hasAria('label', 'Select all rows'); + assert.dom(rowCheckboxesSelector).hasAria('label', 'Select row'); + + await click(selectAllCheckboxSelector); + await click(rowCheckboxesSelector); + + assert.dom(selectAllCheckboxSelector).hasAria('label', 'Select all rows'); + assert.dom(rowCheckboxesSelector).hasAria('label', 'Select row'); + }); + + test('it renders the expected `aria-label` for rows with `@selectionAriaLabelSuffix`', async function (assert) { + await createSelectableTable({ + selectionAriaLabelSuffix: 'custom suffix', + }); + + assert + .dom(rowCheckboxesSelector) + .hasAria('label', 'Select custom suffix'); + + await click(rowCheckboxesSelector); + + assert + .dom(rowCheckboxesSelector) + .hasAria('label', 'Select custom suffix'); + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/nested-rows-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/nested-rows-test.gts new file mode 100644 index 00000000000..66939a9cdc5 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/nested-rows-test.gts @@ -0,0 +1,431 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { click, findAll, render, setupOnerror } from '@ember/test-helpers'; +import { get } from '@ember/helper'; +import type { Target } from '@ember/test-helpers'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +const DEFAULT_NESTED_MODEL = [ + { + id: 1, + name: 'Policy set 1', + status: 'PASS', + description: '', + children: [ + { + id: 11, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + { + id: 12, + name: 'test-hard-mandatory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + { + id: 2, + name: 'Policy set 2', + status: 'FAIL', + description: '', + children: [ + { + id: 21, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + children: [ + { + id: 211, + name: 'test-advisory-pass.sentinel.primary', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + ], + }, +]; + +const DEFAULT_NESTED_COLUMNS = [ + { key: 'name', label: 'Name', isExpandable: true }, + { key: 'status', label: 'Status' }, + { key: 'description', label: 'Description' }, +]; + +const createNestedTable = async (options: { + hasReorderableColumns?: boolean; + isStriped?: boolean; + hasResizableColumns?: boolean; + hasStickyFirstColumn?: boolean; + isSelectable?: boolean; + isSortable?: boolean; + model?: Record[]; +}) => { + const columns = DEFAULT_NESTED_COLUMNS.map((col) => { + if (options.isSortable) { + return { ...col, isSortable: true }; + } + return { ...col }; + }); + + const model = options.model ?? DEFAULT_NESTED_MODEL; + + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('nested rows', function () { + test('it throws an assertion if @hasReorderableColumns and has nested rows', async function (assert) { + const errorMessage = + 'Cannot have reorderable columns if there are nested rows.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + hasReorderableColumns: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it throws an assertion if @isStriped and has nested rows', async function (assert) { + const errorMessage = + '@isStriped must not be true if there are nested rows.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + isStriped: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it throws an assertion if @hasResizableColumns and has nested rows', async function (assert) { + const errorMessage = + 'Cannot have resizable columns if there are nested rows.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + hasResizableColumns: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it throws an assertion if there are sortable columns and has nested rows', async function (assert) { + const errorMessage = + 'Cannot have sortable columns if there are nested rows. Sortable columns are Name,Status,Description'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + isSortable: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it throws an assertion if it has `@hasStickyFirstColumn` and has nested rows', async function (assert) { + const errorMessage = + 'Cannot have a sticky first column if there are nested rows.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + hasStickyFirstColumn: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it throws an assertion if @isSelectable and has nested rows', async function (assert) { + const errorMessage = + '@isSelectable must not be true if there are nested rows.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createNestedTable({ + isSelectable: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + const expandRowButtonSelector = + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__th[role="rowheader"] .hds-advanced-table__th-button--expand'; + + test('it renders a nested table when the model has rows with children key', async function (assert) { + await createNestedTable({}); + + assert.dom(expandRowButtonSelector).exists({ count: 3 }); + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr', + ) + .exists({ count: 6 }); + }); + + test('it renders children rows when click the expand toggle button', async function (assert) { + await createNestedTable({}); + + const rowToggles = findAll(expandRowButtonSelector); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .exists({ count: 4 }); + + if (rowToggles[0]) { + await click(rowToggles[0]); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .exists({ count: 2 }); + } + + if (rowToggles[1]) { + await click(rowToggles[1]); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .exists({ count: 1 }); + } + }); + + test('it renders expanded children rows when pass isOpen in the model', async function (assert) { + const model = [ + { + id: 1, + name: 'Policy set 1', + status: 'PASS', + description: '', + isOpen: true, + children: [ + { + id: 11, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + { + id: 12, + name: 'test-hard-mandatory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + { + id: 2, + name: 'Policy set 2', + status: 'FAIL', + description: '', + isOpen: true, + children: [ + { + id: 21, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + isOpen: true, + children: [ + { + id: 211, + name: 'test-advisory-pass.sentinel.primary', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + ], + }, + ]; + + await createNestedTable({ model }); + + assert.dom(expandRowButtonSelector).exists({ count: 3 }); + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr', + ) + .exists({ count: 6 }); + }); + + test('it renders an expand all button when pass isExpandable to the columns', async function (assert) { + const model = [ + { + id: 1, + name: 'Policy set 1', + status: 'PASS', + description: '', + isOpen: true, + children: [ + { + id: 11, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + { + id: 12, + name: 'test-hard-mandatory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + { + id: 2, + name: 'Policy set 2', + status: 'FAIL', + description: '', + children: [ + { + id: 21, + name: 'test-advisory-pass.sentinel', + status: 'PASS', + description: 'Sample description for this thing.', + children: [ + { + id: 211, + name: 'test-advisory-pass.sentinel.primary', + status: 'PASS', + description: 'Sample description for this thing.', + }, + ], + }, + ], + }, + ]; + + await createNestedTable({ model }); + + const expandAllButton = document.querySelector( + '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', + ); + + if (expandAllButton) { + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', + ) + .exists({ count: 1 }); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .exists({ count: 2 }); + assert.dom(expandAllButton).hasAria('expanded', 'false'); + + await click(expandAllButton); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .doesNotExist(); + assert.dom(expandAllButton).hasAria('expanded', 'true'); + + await click(expandAllButton); + + assert + .dom( + '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', + ) + .exists({ count: 4 }); + assert.dom(expandAllButton).hasAria('expanded', 'false'); + } + }); + + test('the expand all button state updates when expand buttons are clicked', async function (assert) { + await createNestedTable({}); + + const rowToggles = findAll(expandRowButtonSelector); + const expandAllButton = document.querySelector( + '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', + ); + + assert.dom(expandAllButton).hasAria('expanded', 'false'); + + for (let i = 0; i < rowToggles.length; i++) { + await click(rowToggles[i] as Target); + + if (i < rowToggles.length - 1) { + assert.dom(expandAllButton).hasAria('expanded', 'false'); + } + } + + assert.dom(expandAllButton).hasAria('expanded', 'true'); + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/sorting-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/sorting-test.gts new file mode 100644 index 00000000000..a399a7380a7 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/sorting-test.gts @@ -0,0 +1,411 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { array, hash, get } from '@ember/helper'; +import { click, focus, render } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; +import sinon from 'sinon'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { + HdsAdvancedTableColumn, + HdsAdvancedTableDensities, + HdsAdvancedTableOnSelectionChangeSignature, + HdsAdvancedTableThSortOrder, + HdsAdvancedTableVerticalAlignment, +} from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; +import type { FolkMusic } from 'showcase/mocks/folk-music-data'; + +const DEFAULT_SORTABLE_MODEL = [ + { + id: '1', + type: 'folk', + artist: 'Nick Drake', + album: 'Pink Moon', + year: '1972', + }, + { + id: '2', + type: 'folk', + artist: 'The Beatles', + album: 'Abbey Road', + year: '1969', + }, + { + id: '3', + type: 'folk', + artist: 'Melanie', + album: 'Candles in the Rain', + year: '1971', + }, +]; + +const DEFAULT_SORTABLE_COLUMNS = [ + { key: 'artist', label: 'Artist', isSortable: true }, + { key: 'album', label: 'Album', isSortable: true }, + { key: 'year', label: 'Year' }, +]; + +const createSortableTable = async (options: { + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; + sortedMessageText?: string; + caption?: string; + hasStickyFirstColumn?: boolean; + density?: HdsAdvancedTableDensities; + valign?: HdsAdvancedTableVerticalAlignment; + maxHeight?: string; + hasStickyHeader?: boolean; + hasTooltip?: boolean; + columns?: HdsAdvancedTableColumn[]; +}) => { + const columns = DEFAULT_SORTABLE_COLUMNS.map((col, index) => { + if (options.hasTooltip && index === 0) { + return { ...col, tooltip: 'More info.' }; + } + return col; + }); + + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('sorting', function () { + test('it should render a sortable table when appropriate', async function (assert) { + await createSortableTable({ + sortBy: 'artist', + sortOrder: 'asc', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__th:first-of-type') + .hasClass('hds-advanced-table__th--sort'); + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__th:first-of-type .hds-advanced-table__th-content > span', + ) + .hasText('Artist'); + }); + + test('it should render a sortable table with a tooltip', async function (assert) { + await createSortableTable({ + sortBy: 'artist', + sortOrder: 'asc', + hasTooltip: true, + }); + + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th:first-of-type .hds-advanced-table__th-button--tooltip', + ) + .exists(); + // activate the tooltip: + await focus( + '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th:first-of-type .hds-advanced-table__th-button--tooltip', + ); + // test that the tooltip exists and has the passed in content: + assert.dom('.tippy-content').hasText('More info.'); + }); + + test('it should render a sortable table and table is unsorted', async function (assert) { + await createSortableTable({}); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__th:first-of-type') + .hasClass('hds-advanced-table__th--sort'); + assert + .dom('#data-test-advanced-table .hds-advanced-table__caption') + .hasText(''); + }); + + test('it updates the caption correctly after a sort has been performed', async function (assert) { + await createSortableTable({}); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('Nick Drake'); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('Melanie'); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__caption') + .hasText('Sorted by artist ascending'); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('The Beatles'); + assert + .dom('#data-test-advanced-table .hds-advanced-table__caption') + .hasText('Sorted by artist descending'); + }); + + test('it sorts the rows asc by default when the sort button is clicked on an unsorted column', async function (assert) { + await createSortableTable({ + sortBy: 'artist', + sortOrder: 'asc', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('Melanie'); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('The Beatles'); + }); + + test('it renders a custom sortedMessageText if supplied', async function (assert) { + await createSortableTable({ + sortedMessageText: 'Melanie will sort it', + sortBy: 'artist', + sortOrder: 'asc', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__caption') + .hasText('Melanie will sort it'); + }); + + test('it renders both a custom caption and a custom sortedMessageText if supplied', async function (assert) { + await createSortableTable({ + caption: 'A custom caption.', + sortedMessageText: 'Melanie will sort it!', + sortBy: 'artist', + sortOrder: 'asc', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__caption') + .hasText('A custom caption. Melanie will sort it!'); + }); + + test('it uses a custom sort function if one is supplied', async function (assert) { + // contrived example; we don’t care _what_ the custom sorting function does, just that it’s used instead of the default. + // sort based on the second letter of the album name + const mySortingFunction = (a: unknown, b: unknown) => { + const typedA = a as FolkMusic; + const typedB = b as FolkMusic; + + if (typedA.album.charAt(1) < typedB.album.charAt(1)) { + return -1; + } else if (typedA.album.charAt(1) > typedB.album.charAt(1)) { + return 1; + } else { + return 0; + } + }; + + const columns = [ + { key: 'artist', label: 'Artist', isSortable: true }, + { + key: 'album', + label: 'Album', + isSortable: true, + sortingFunction: mySortingFunction, + }, + { key: 'year', label: 'Year' }, + ]; + + await createSortableTable({ + columns, + sortBy: 'album', + sortOrder: 'asc', + }); + // let’s just check that the table is pre-sorted the way we expect (artist, ascending) + assert + .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') + .hasText('Melanie'); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(2) button', + ); + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__td:nth-of-type(2)', + ) + .hasText('Candles in the Rain'); + }); + + test('it updates the `aria-sort` attribute value when a sort is performed', async function (assert) { + await createSortableTable({ + sortBy: 'artist', + sortOrder: 'asc', + }); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1)', + ) + .hasAria('sort', 'descending'); + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1)', + ) + .hasAria('sort', 'ascending'); + }); + + test('it invokes the `onSort` callback when a sort is performed', async function (assert) { + const context = new TrackedObject<{ + sortBy?: string; + sortOrder?: HdsAdvancedTableThSortOrder; + }>({ + sortBy: 'artist', + sortOrder: 'asc', + }); + + const onSort = ( + sortBy: string, + sortOrder: HdsAdvancedTableThSortOrder, + ) => { + context.sortBy = sortBy; + context.sortOrder = sortOrder; + }; + + await createSortableTable({ + sortBy: context.sortBy, + sortOrder: context.sortOrder, + onSort, + }); + + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert.strictEqual(context.sortBy, 'artist'); + assert.strictEqual(context.sortOrder, 'desc'); + await click( + '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', + ); + assert.strictEqual(context.sortBy, 'artist'); + assert.strictEqual(context.sortOrder, 'asc'); + }); + + test('it sorts by selected row when `@selectableColumnKey` is provided', async function (assert) { + const sortSpy = sinon.spy(); + + const sortBySelectedSelector = + '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th[role="columnheader"] .hds-advanced-table__th-button--sort'; + + const model = [ + { id: '1', name: 'Bob', age: 1, isSelected: false }, + { id: '2', name: 'Sally', age: 50, isSelected: true }, + { id: '3', name: 'Jim', age: 30, isSelected: false }, + ]; + + const onSelectionChange = ({ + selectionKey, + }: HdsAdvancedTableOnSelectionChangeSignature) => { + const recordToUpdate = model.find( + (modelRow) => modelRow.id === selectionKey, + ); + if (recordToUpdate) { + recordToUpdate.isSelected = !recordToUpdate.isSelected; + } + }; + + await render( + , + ); + + assert.dom(sortBySelectedSelector).exists(); + + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr:nth-of-type(3) .hds-advanced-table__td', + ) + .hasText('Jim'); + + await click(sortBySelectedSelector); + assert + .dom( + '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr:nth-of-type(3) .hds-advanced-table__td', + ) + .hasText('Sally'); + + assert.ok( + sortSpy.calledWith('isSelected', 'asc'), + 'it invokes the `onSort` callback with the `selectableColumnKey` when a sort is performed on the selectable column', + ); + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/features/sticky-test.gts b/showcase/tests/integration/components/hds/advanced-table/features/sticky-test.gts new file mode 100644 index 00000000000..5d2cbb4c4a6 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/features/sticky-test.gts @@ -0,0 +1,504 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { + click, + find, + findAll, + render, + setupOnerror, +} from '@ember/test-helpers'; +import { get } from '@ember/helper'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { + HdsAdvancedTableColumn, + HdsAdvancedTableDensities, + HdsAdvancedTableOnSelectionChangeSignature, + HdsAdvancedTableVerticalAlignment, +} from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +async function performContextMenuAction(th: Element | null, key: string) { + const contextMenuToggle = th?.querySelector('.hds-dropdown-toggle-icon'); + + if (contextMenuToggle) { + await click(contextMenuToggle); + return click(`[data-test-context-option-key="${key}"]`); + } +} + +const DEFAULT_BASIC_MODEL = [ + { id: '1', name: 'Bob', age: 20, country: 'USA' }, + { id: '2', name: 'Alice', age: 25, country: 'UK' }, + { id: '3', name: 'Charlie', age: 30, country: 'Canada' }, +]; + +const DEFAULT_BASIC_COLUMNS = [ + { key: 'name', label: 'Name' }, + { key: 'age', label: 'Age' }, + { key: 'country', label: 'Country' }, +]; + +const createBasicTable = async (options: { + hasStickyFirstColumn?: boolean; + maxHeight?: string; + hasStickyHeader?: boolean; +}) => { + return await render( + , + ); +}; + +const DEFAULT_SORTABLE_MODEL = [ + { + id: '1', + type: 'folk', + artist: 'Nick Drake', + album: 'Pink Moon', + year: '1972', + }, + { + id: '2', + type: 'folk', + artist: 'The Beatles', + album: 'Abbey Road', + year: '1969', + }, + { + id: '3', + type: 'folk', + artist: 'Melanie', + album: 'Candles in the Rain', + year: '1971', + }, +]; + +const DEFAULT_SORTABLE_COLUMNS = [ + { key: 'artist', label: 'Artist', isSortable: true }, + { key: 'album', label: 'Album', isSortable: true }, + { key: 'year', label: 'Year' }, +]; + +const createSortableTable = async (options: { + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; + sortedMessageText?: string; + caption?: string; + hasStickyFirstColumn?: boolean; + density?: HdsAdvancedTableDensities; + valign?: HdsAdvancedTableVerticalAlignment; + maxHeight?: string; + hasStickyHeader?: boolean; + hasTooltip?: boolean; + columns?: HdsAdvancedTableColumn[]; +}) => { + const columns = DEFAULT_SORTABLE_COLUMNS.map((col, index) => { + if (options.hasTooltip && index === 0) { + return { ...col, tooltip: 'More info.' }; + } + return col; + }); + + return await render( + , + ); +}; + +const DEFAULT_SELECTABLE_MODEL = [ + { + id: '1', + type: 'folk', + artist: 'Nick Drake', + album: 'Pink Moon', + year: '1972', + }, + { + id: '2', + type: 'folk', + artist: 'The Beatles', + album: 'Abbey Road', + year: '1969', + }, + { + id: '3', + type: 'folk', + artist: 'Melanie', + album: 'Candles in the Rain', + year: '1971', + }, +]; + +const DEFAULT_SELECTABLE_COLUMNS = [ + { key: 'artist', label: 'Artist' }, + { key: 'album', label: 'Album' }, + { key: 'year', label: 'Year' }, +]; + +const createSelectableTable = async (options: { + selectionAriaLabelSuffix?: string; + hasStickyFirstColumn?: boolean; + onSelectionChange?: ( + args: HdsAdvancedTableOnSelectionChangeSignature, + ) => void; +}) => { + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + module('sticky header & columns', function () { + test('it should render with a CSS class appropriate for the @hasStickyHeader argument', async function (assert) { + await createBasicTable({ + hasStickyHeader: true, + maxHeight: '75px', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__thead') + .hasClass('hds-advanced-table__thead--sticky'); + }); + + test('it should render the appropriate CSS and add a sticky header when set @maxHeight', async function (assert) { + await createBasicTable({ + maxHeight: '75px', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__thead') + .hasClass('hds-advanced-table__thead--sticky'); + + assert + .dom('#data-test-advanced-table .hds-advanced-table') + .hasStyle({ maxHeight: '75px' }); + }); + + test('it should render the appropriate CSS when set @maxHeight and @hasStickyHeader is set to false', async function (assert) { + await createBasicTable({ + hasStickyHeader: false, + maxHeight: '75px', + }); + + assert + .dom('#data-test-advanced-table .hds-advanced-table__thead') + .doesNotHaveClass('hds-advanced-table__thead--sticky'); + + assert + .dom('#data-test-advanced-table .hds-advanced-table') + .hasStyle({ maxHeight: '75px' }); + }); + + test('it throws an assertion if it has `@hasStickyHeader` and does not have @maxHeight', async function (assert) { + const errorMessage = 'Must set @maxHeight to use @hasStickyHeader.'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await createBasicTable({ + hasStickyHeader: true, + }); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + + test('it should render with a CSS class appropriate for the @hasStickyFirstColumn argument', async function (assert) { + await createSortableTable({ + hasStickyFirstColumn: true, + }); + + assert + .dom( + '.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column.hds-advanced-table__th--sort', + ) + .exists({ count: 1 }); + + assert.dom('.hds-advanced-table__th').exists({ count: 3 }); + }); + + test('it should render with a CSS class appropriate for the @hasStickyFirstColumn argument when also selectable', async function (assert) { + await createSelectableTable({ + hasStickyFirstColumn: true, + }); + + assert + .dom( + '.hds-advanced-table__th--is-selectable.hds-advanced-table__th--is-sticky-column', + ) + .exists({ count: 4 }); + + assert + .dom( + '.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column:not(.hds-advanced-table__th--is-selectable)', + ) + .exists({ count: 4 }); + }); + + test('it should show the context menu when the @hasStickyFirstColumn argument is true', async function (assert) { + await createBasicTable({ + hasStickyFirstColumn: true, + }); + + const ths = findAll('.hds-advanced-table__th'); + const firstTh = ths[1]; // find the first header cell after the selectable column + + if (firstTh) { + assert.ok( + firstTh.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const contextMenuToggle = firstTh.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (contextMenuToggle) { + await click(contextMenuToggle); + + assert + .dom('[data-test-context-option-key="pin-first-column"]') + .exists(); + } + } + }); + + test('it should show the context menu when the @hasStickyFirstColumn argument is false', async function (assert) { + await createBasicTable({ + hasStickyFirstColumn: false, + }); + + const ths = findAll('.hds-advanced-table__th'); + const firstTh = ths[1]; // find the first header cell after the selectable column + + if (firstTh) { + assert.ok( + firstTh.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const contextMenuToggle = firstTh.querySelector( + '.hds-dropdown-toggle-icon', + ); + + if (contextMenuToggle) { + await click(contextMenuToggle); + assert + .dom('[data-test-context-option-key="pin-first-column"]') + .exists(); + } + } + }); + + test('it should not show the context menu when the @hasStickyFirstColumn argument is undefined', async function (assert) { + await createBasicTable({}); + + const ths = findAll('.hds-advanced-table__th'); + const firstTh = ths[1]; // find the first header cell after the selectable column + + if (firstTh) { + assert.notOk( + firstTh.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + } + }); + + test('it should toggle column pinning when the context menu item is clicked', async function (assert) { + await createBasicTable({ + hasStickyFirstColumn: false, + }); + + const ths = findAll('.hds-advanced-table__th'); + const firstTh = ths[1]; // find the first header cell after the selectable column + + if (firstTh) { + // Pin column + await performContextMenuAction(firstTh, 'pin-first-column'); + + assert + .dom( + '.hds-advanced-table__thead .hds-advanced-table__th.hds-advanced-table__th--is-sticky-column', + ) + .exists({ count: 2 }); + + // Unpin column + await performContextMenuAction(firstTh, 'pin-first-column'); + + assert + .dom( + '.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column', + ) + .doesNotExist(); + } + }); + + test('it should show the context menu when the @hasStickyFirstColumn argument is true and the column is sortable', async function (assert) { + await createSortableTable({ + hasStickyFirstColumn: true, + }); + + const th = find('.hds-advanced-table__th--sort'); // find the first header cell + + if (th) { + assert.ok( + th.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); + + if (contextMenuToggle) { + await click(contextMenuToggle); + + assert + .dom('[data-test-context-option-key="pin-first-column"]') + .exists(); + } + } + }); + + test('it should show the context menu when the @hasStickyFirstColumn argument is false and the column is sortable', async function (assert) { + await createSortableTable({ + hasStickyFirstColumn: false, + }); + + const th = find('.hds-advanced-table__th--sort'); // find the first header cell + + if (th) { + assert.ok( + th.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + + const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); + + if (contextMenuToggle) { + await click(contextMenuToggle); + + assert + .dom('[data-test-context-option-key="pin-first-column"]') + .exists(); + } + } + }); + + test('it should not show the context menu when the @hasStickyFirstColumn argument is undefined', async function (assert) { + await createBasicTable({}); + + const ths = findAll('.hds-advanced-table__th'); + const firstTh = ths[1]; // find the first header cell after the selectable column + + if (firstTh) { + assert.notOk( + firstTh.querySelector('.hds-advanced-table__th-context-menu'), + 'context menu exists', + ); + } + }); + + test('it should toggle column pinning when the context menu item is clicked and the column is sortable', async function (assert) { + await createSortableTable({ + hasStickyFirstColumn: false, + }); + + const th = find('.hds-advanced-table__th--sort'); // find the first header cell + + // Pin column + await performContextMenuAction(th, 'pin-first-column'); + + assert + .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') + .exists({ count: 1 }); + + // Unpin column + await performContextMenuAction(th, 'pin-first-column'); + + assert + .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') + .doesNotExist(); + }); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/index-test.gts b/showcase/tests/integration/components/hds/advanced-table/index-test.gts new file mode 100644 index 00000000000..bed6799e113 --- /dev/null +++ b/showcase/tests/integration/components/hds/advanced-table/index-test.gts @@ -0,0 +1,378 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { array, hash, get } from '@ember/helper'; +import { findAll, render, settled, resetOnerror } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsAdvancedTable } from '@hashicorp/design-system-components/components'; +import type { + HdsAdvancedTableDensities, + HdsAdvancedTableVerticalAlignment, +} from '@hashicorp/design-system-components/components/hds/advanced-table/types'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +const getBodyContent = () => { + return Array.from( + document.querySelectorAll( + '.hds-advanced-table__tbody .hds-advanced-table__tr', + ), + ).map((row) => { + const cells = row.querySelectorAll('.hds-advanced-table__td'); + return Array.from(cells).map((cell) => cell.textContent.trim()); + }); +}; + +const getColumnByLabel = ( + columns: typeof DEFAULT_BASIC_COLUMNS, + label: string, +) => { + return columns.find((col) => col.label === label); +}; + +const getColumnOrder = (columns: typeof DEFAULT_BASIC_COLUMNS) => { + const thElements = findAll('.hds-advanced-table__th'); + + return thElements.map((th) => { + const column = getColumnByLabel(columns, th.textContent.trim()); + + return column ? column.key : null; + }); +}; + +const DEFAULT_BASIC_MODEL = [ + { id: '1', name: 'Bob', age: 20, country: 'USA' }, + { id: '2', name: 'Alice', age: 25, country: 'UK' }, + { id: '3', name: 'Charlie', age: 30, country: 'Canada' }, +]; + +const DEFAULT_BASIC_COLUMNS = [ + { key: 'name', label: 'Name' }, + { key: 'age', label: 'Age' }, + { key: 'country', label: 'Country' }, +]; + +const createBasicTable = async (options: { + hasStickyFirstColumn?: boolean; + density?: HdsAdvancedTableDensities; + valign?: HdsAdvancedTableVerticalAlignment; +}) => { + return await render( + , + ); +}; + +module('Integration | Component | hds/advanced-table/index', function (hooks) { + setupRenderingTest(hooks); + + hooks.afterEach(function () { + resetOnerror(); + }); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + await createBasicTable({}); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table'); + }); + + test('it should render with a CSS class appropriate for the @density value', async function (assert) { + await createBasicTable({ + density: 'short', + }); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table--density-short'); + }); + + test('it should render with a CSS class appropriate if no @density value is set', async function (assert) { + await createBasicTable({}); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table--density-medium'); + }); + + test('it should render with a CSS class appropriate for middle @valign value', async function (assert) { + await createBasicTable({ + valign: 'middle', + }); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table--valign-middle'); + }); + + test('it should render with a CSS class appropriate baseline @valign value', async function (assert) { + await createBasicTable({ + valign: 'baseline', + }); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table--valign-baseline'); + }); + + test('it should render with a CSS class appropriate if no @valign value is set', async function (assert) { + await createBasicTable({}); + + assert + .dom('#data-test-advanced-table [role="grid"]') + .hasClass('hds-advanced-table--valign-top'); + }); + + test('it should support splattributes', async function (assert) { + await render( + , + ); + + assert + .dom('#data-test-advanced-table') + .hasAttribute('aria-label', 'data test table'); + }); + + test('it should render a table based on the data model passed', async function (assert) { + await render( + , + ); + + assert + .dom('#data-advanced-test-table .hds-advanced-table__tr:nth-child(3)') + .hasProperty('id', '2'); + + assert + .dom( + '#data-advanced-test-table .hds-advanced-table__tr:first-of-type .hds-advanced-table__td:nth-of-type(2n)', + ) + .hasText('Test 1'); + assert + .dom( + '#data-advanced-test-table .hds-advanced-table__tr:last-of-type .hds-advanced-table__td:last-of-type', + ) + .hasText('Test 3 description'); + }); + + test('it should update the table when the model changes', async function (assert) { + const context = new TrackedObject({ + model: DEFAULT_BASIC_MODEL, + }); + + await render( + , + ); + + assert + .dom(`.hds-advanced-table__tbody .hds-advanced-table__tr`) + .exists({ count: 3 }); + assert.deepEqual(getBodyContent(), [ + ['Bob', '20', 'USA'], + ['Alice', '25', 'UK'], + ['Charlie', '30', 'Canada'], + ]); + + context.model = [{ id: '4', name: 'Jane', age: 35, country: 'Mexico' }]; + await settled(); + + assert + .dom(`.hds-advanced-table__tbody .hds-advanced-table__tr`) + .exists({ count: 1 }); + assert.deepEqual(getBodyContent(), [['Jane', '35', 'Mexico']]); + }); + + test('it should update the table when the columns change', async function (assert) { + const context = new TrackedObject({ + columns: DEFAULT_BASIC_COLUMNS, + }); + + const getColumnLabels = () => { + return Array.from( + document.querySelectorAll( + '.hds-advanced-table__thead .hds-advanced-table__th', + ), + ).map((th) => th.textContent.trim()); + }; + + await render( + , + ); + + assert.deepEqual(getColumnLabels(), ['Name', 'Age', 'Country']); + + context.columns = context.columns.map((column) => ({ + ...column, + label: `Updated ${column.label}`, + })); + await settled(); + + assert.deepEqual(getColumnLabels(), [ + 'Updated Name', + 'Updated Age', + 'Updated Country', + ]); + }); + + test('it should render correct columns when columns are added or removed dynamically', async function (assert) { + const context = new TrackedObject({ + columns: DEFAULT_BASIC_COLUMNS, + }); + + await render( + , + ); + + let columnOrder = getColumnOrder(context.columns); + assert.deepEqual( + columnOrder, + ['name', 'age', 'country'], + 'Initial columns are correct', + ); + + assert.deepEqual(getBodyContent(), [ + ['Bob', '20', 'USA'], + ['Alice', '25', 'UK'], + ['Charlie', '30', 'Canada'], + ]); + + context.columns = context.columns.filter((col) => col.key !== 'age'); + + await settled(); + + columnOrder = getColumnOrder(context.columns); + assert.deepEqual( + columnOrder, + ['name', 'country'], + 'Columns are correct after removing age', + ); + assert.deepEqual(getBodyContent(), [ + ['Bob', 'USA'], + ['Alice', 'UK'], + ['Charlie', 'Canada'], + ]); + + context.columns = DEFAULT_BASIC_COLUMNS; + await settled(); + + columnOrder = getColumnOrder(context.columns); + assert.deepEqual( + columnOrder, + ['name', 'age', 'country'], + 'Columns are correct after adding age back', + ); + assert.deepEqual(getBodyContent(), [ + ['Bob', '20', 'USA'], + ['Alice', '25', 'UK'], + ['Charlie', '30', 'Canada'], + ]); + }); +}); diff --git a/showcase/tests/integration/components/hds/advanced-table/index-test.js b/showcase/tests/integration/components/hds/advanced-table/index-test.js deleted file mode 100644 index a2a3e91adcc..00000000000 --- a/showcase/tests/integration/components/hds/advanced-table/index-test.js +++ /dev/null @@ -1,2776 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; -import { - render, - click, - focus, - setupOnerror, - find, - findAll, - settled, - triggerEvent, - triggerKeyEvent, -} from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import sinon from 'sinon'; - -function gridValuesAreEqual(newGridValues, originalGridValues) { - return newGridValues.every((newGridValue, index) => { - const newGridValueInt = parseInt(newGridValue, 10); - const originalGridValueInt = parseInt(originalGridValues[index], 10); - - // Allow for small pixel differences due to CSS grid subpixel rendering in different environments - return Math.abs(newGridValueInt - originalGridValueInt) <= 1; - }); -} - -function getTableGridValues(tableElement) { - const computedStyle = window.getComputedStyle(tableElement); - const gridTemplateColumns = computedStyle.getPropertyValue( - 'grid-template-columns', - ); - const gridValues = gridTemplateColumns - .split(' ') - .map((value) => value.trim()); - - return gridValues; -} - -function getBodyContent() { - return Array.from( - document.querySelectorAll( - '.hds-advanced-table__tbody .hds-advanced-table__tr', - ), - ).map((row) => { - const cells = row.querySelectorAll('.hds-advanced-table__td'); - return Array.from(cells).map((cell) => cell.textContent.trim()); - }); -} - -async function performContextMenuAction(th, key) { - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - - await click(contextMenuToggle); - - return click(`[data-test-context-option-key="${key}"]`); -} - -async function simulateRightPointerDrag(handle) { - await triggerEvent(handle, 'pointerdown', { clientX: 100, button: 0 }); - await triggerEvent(handle, 'pointermove', { clientX: 130, buttons: 1 }); - await triggerEvent(window, 'pointerup', { button: 0 }); -} - -function getColumnByLabel(columns, label) { - return columns.find((col) => col.label === label); -} - -async function getColumnOrder(columns) { - const thElements = await findAll('.hds-advanced-table__th'); - - return thElements.map((th) => { - const column = getColumnByLabel(columns, th.textContent.trim()); - - return column ? column.key : null; - }); -} - -async function startReorderDrag(handleElement) { - return triggerEvent(handleElement, 'dragstart'); -} - -function getTargetElementFromColumnIndex(index) { - const dropTargets = findAll('.hds-advanced-table__th-reorder-drop-target'); - const target = dropTargets[index]; - - if (target === null) { - throw new Error( - `Target column at index ${index} not found after drag started.`, - ); - } - - return target; -} - -function getDragTargetPosition(targetElement, targetPosition) { - const targetRect = targetElement.getBoundingClientRect(); - let clientX; - - switch (targetPosition) { - case 'left': - clientX = targetRect.left + 1; - break; - case 'right': - clientX = targetRect.right - 1; - break; - default: - throw new Error( - `Invalid targetPosition: ${targetPosition}. Use 'left' or 'right'.`, - ); - } - - return { clientX, clientY: targetRect.top + targetRect.height / 2 }; -} - -async function dragOverTarget(target, { clientX, clientY }) { - await triggerEvent(target, 'dragenter', { clientX, clientY }); - await triggerEvent(target, 'dragover', { clientX, clientY }); -} - -async function simulateColumnReorderDrag({ - handleElement, - targetElement, - targetIndex, - targetPosition = 'left', -}) { - await startReorderDrag(handleElement); - - const target = targetElement ?? getTargetElementFromColumnIndex(targetIndex); - const { clientX, clientY } = getDragTargetPosition(target, targetPosition); - - const eventOptions = { clientX, clientY }; - - await dragOverTarget(target, eventOptions); - - // return the target event options for further use, if needed - return { target, eventOptions }; -} - -async function simulateColumnReorderDrop({ - target, - handleElement, - eventOptions, -}) { - await triggerEvent(target, 'drop', eventOptions); - await triggerEvent(handleElement, 'dragend'); -} - -// we're using this for multiple tests so we'll declare context once and use it when we need it. -const setTableData = (context) => { - context.set('model', [ - { name: 'Bob', age: 20, country: 'USA' }, - { name: 'Alice', age: 25, country: 'UK' }, - { name: 'Charlie', age: 30, country: 'Canada' }, - ]); -}; -const setSortableTableData = (context) => { - context.set('model', [ - { - id: '1', - type: 'folk', - artist: 'Nick Drake', - album: 'Pink Moon', - year: '1972', - }, - { - id: '2', - type: 'folk', - artist: 'The Beatles', - album: 'Abbey Road', - year: '1969', - }, - { - id: '3', - type: 'folk', - artist: 'Melanie', - album: 'Candles in the Rain', - year: '1971', - }, - ]); - context.set('columns', [ - { key: 'artist', label: 'Artist', isSortable: true }, - { key: 'album', label: 'Album', isSortable: true }, - { key: 'year', label: 'Year' }, - ]); - context.set('sortBy', 'artist'); - context.set('sortOrder', 'asc'); -}; - -const setSelectableTableData = (context) => { - context.set('model', [ - { - id: '1', - type: 'folk', - artist: 'Nick Drake', - album: 'Pink Moon', - year: '1972', - }, - { - id: '2', - type: 'folk', - artist: 'The Beatles', - album: 'Abbey Road', - year: '1969', - }, - { - id: '3', - type: 'folk', - artist: 'Melanie', - album: 'Candles in the Rain', - year: '1971', - }, - ]); - context.set('columns', [ - { key: 'artist', label: 'Artist' }, - { key: 'album', label: 'Album' }, - { key: 'year', label: 'Year' }, - ]); -}; - -const setNestedTableData = (context) => { - context.set('model', [ - { - id: 1, - name: 'Policy set 1', - status: 'PASS', - description: '', - children: [ - { - id: 11, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - { - id: 12, - name: 'test-hard-mandatory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - { - id: 2, - name: 'Policy set 2', - status: 'FAIL', - description: '', - children: [ - { - id: 21, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - children: [ - { - id: 211, - name: 'test-advisory-pass.sentinel.primary', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - ], - }, - ]); - context.set('columns', [ - { key: 'name', label: 'Name', isExpandable: true }, - { key: 'status', label: 'Status' }, - { key: 'description', label: 'Description' }, - ]); -}; - -const setReorderableColumnsTableData = (context) => { - context.set('model', [ - { id: '1', artist: 'Nick Drake', album: 'Pink Moon', year: '1972' }, - { id: '2', artist: 'The Beatles', album: 'Abbey Road', year: '1969' }, - { id: '3', artist: 'Melanie', album: 'Candles in the Rain', year: '1971' }, - ]); - context.set('columns', [ - { key: 'artist', label: 'Artist' }, - { key: 'album', label: 'Album' }, - { key: 'year', label: 'Year' }, - ]); -}; - -const setResizableColumnsTableData = (context) => { - context.set('model', [ - { id: '1', col1: 'A', col2: 'B' }, - { id: '2', col1: 'C', col2: 'D' }, - ]); - context.set('columns', [ - { - key: 'col1', - label: 'Col 1', - width: '120px', - minWidth: '60px', - maxWidth: '300px', - }, - { - key: 'col2', - label: 'Col 2', - }, - ]); -}; - -const hbsAdvancedTable = hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - {{B.data.country}} - - -`; - -const hbsSortableAdvancedTable = hbs` - <:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`; - -const hbsSelectableAdvancedTable = hbs` - <:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`; - -const hbsNestedAdvancedTable = hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.status}} - {{B.data.description}} - - -`; - -const hbsResizableColumnsAdvancedTable = hbs`
- - <:body as |B|> - - {{B.data.col1}} - {{B.data.col2}} - - -
`; - -module('Integration | Component | hds/advanced-table/index', function (hooks) { - setupRenderingTest(hooks); - - module('column reordering', function (hooks) { - hooks.beforeEach(function () { - setReorderableColumnsTableData(this); - }); - - test('it renders reorder handles when reordering is enabled', async function (assert) { - this.set('hasReorderableColumns', false); - - await render( - hbs``, - ); - - assert - .dom('.hds-advanced-table__th-reorder-handle') - .doesNotExist( - 'No reorder handles are rendered when reordering is disabled', - ); - - this.set('hasReorderableColumns', true); - - assert - .dom('.hds-advanced-table__th-reorder-handle') - .exists({ count: 3 }, 'All columns have a reorder handle'); - }); - - test('it does not render a reorder handle on the row selection column', async function (assert) { - await render( - hbs``, - ); - - const selectAllThSelector = - '[role="columnheader"].hds-advanced-table__th--is-selectable'; - const reorderHandleSelector = '.hds-advanced-table__th-reorder-handle'; - - assert - .dom(`${selectAllThSelector} ${reorderHandleSelector}`) - .doesNotExist( - 'No reorder handle is rendered on the row selection column', - ); - }); - - test('columns can be reordered by dragging and dropping', async function (assert) { - await render( - hbs``, - ); - - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - this.columns.map((col) => col.key), - 'Initial column order is correct', - ); - - const expectedDropTargetIndex = 2; - const expectedDropTargetDropSide = 'right'; - - // get the first reorder handle - const reorderHandle = find('.hds-advanced-table__th-reorder-handle'); - - // drag to the right side of the last column - const { target, eventOptions } = await simulateColumnReorderDrag({ - handleElement: reorderHandle, - targetIndex: expectedDropTargetIndex, - targetPosition: expectedDropTargetDropSide, - }); - - // get all drop targets for test reference - const dropTargets = findAll( - '.hds-advanced-table__th-reorder-drop-target', - ); - const originDropTarget = dropTargets[0]; - const destinationDropTarget = dropTargets[expectedDropTargetIndex]; - - assert - .dom(originDropTarget) - .hasClass( - 'hds-advanced-table__th-reorder-drop-target--is-being-dragged', - 'First column is being dragged', - ); - assert - .dom(destinationDropTarget) - .hasClass( - 'hds-advanced-table__th-reorder-drop-target--is-dragging-over', - ) - .hasClass( - `hds-advanced-table__th-reorder-drop-target--is-dragging-over--${expectedDropTargetDropSide}`, - ); - - await simulateColumnReorderDrop({ - target, - handleElement: reorderHandle, - eventOptions, - }); - - columnOrder = await getColumnOrder(this.columns); - - assert - .dom('.hds-advanced-table__th-reorder-drop-target') - .doesNotExist('Drop targets are removed after drop'); - assert.deepEqual( - columnOrder, - [this.columns[1].key, this.columns[2].key, this.columns[0].key], - 'Columns are reordered correctly after drag and drop', - ); - }); - - test('dropping a target on the nearest side of the next sibling does not reorder columns', async function (assert) { - await render( - hbs``, - ); - - const initialColumnOrder = this.columns.map((col) => col.key); - - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - initialColumnOrder, - 'Initial column order is correct', - ); - - const reorderHandle = find('.hds-advanced-table__th-reorder-handle'); - - const { target, eventOptions } = await simulateColumnReorderDrag({ - handleElement: reorderHandle, - targetIndex: 1, - targetPosition: 'left', - }); - - const dropTargets = findAll( - '.hds-advanced-table__th-reorder-drop-target', - ); - const originDropTarget = dropTargets[0]; - const destinationDropTarget = dropTargets[1]; - - assert - .dom(originDropTarget) - .hasClass( - 'hds-advanced-table__th-reorder-drop-target--is-being-dragged', - 'First column is being dragged', - ); - assert - .dom(destinationDropTarget) - .doesNotHaveClass( - 'hds-advanced-table__th-reorder-drop-target--is-dragging-over', - ) - .doesNotHaveClass( - 'hds-advanced-table__th-reorder-drop-target--is-dragging-over--left', - ); - - await simulateColumnReorderDrop({ - target, - handleElement: reorderHandle, - eventOptions, - }); - - columnOrder = await getColumnOrder(this.columns); - - assert - .dom('.hds-advanced-table__th-reorder-drop-target') - .doesNotExist('Drop targets are removed after drop'); - assert.deepEqual( - columnOrder, - initialColumnOrder, - 'Columns order is unchanged after drop on the nearest side', - ); - }); - - test('it should show the context menu with the correct options when reordering is enabled', async function (assert) { - await render( - hbs``, - ); - - const thElements = findAll('.hds-advanced-table__th'); // find all header cells - - assert.ok( - thElements[0].querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const firstContextMenuToggle = thElements[0].querySelector( - '.hds-dropdown-toggle-icon', - ); - await click(firstContextMenuToggle); - assert.dom('[data-test-context-option-key="reorder-column"]').exists(); - assert - .dom('[data-test-context-option-key="move-column-to-start"]') - .doesNotExist(); - assert - .dom('[data-test-context-option-key="move-column-to-end"]') - .exists(); - - const secondContextMenuToggle = thElements[1].querySelector( - '.hds-dropdown-toggle-icon', - ); - await click(secondContextMenuToggle); - assert.dom('[data-test-context-option-key="reorder-column"]').exists(); - assert - .dom('[data-test-context-option-key="move-column-to-start"]') - .exists(); - assert - .dom('[data-test-context-option-key="move-column-to-end"]') - .exists(); - - const lastContextMenuToggle = thElements[ - thElements.length - 1 - ].querySelector('.hds-dropdown-toggle-icon'); - await click(lastContextMenuToggle); - assert.dom('[data-test-context-option-key="reorder-column"]').exists(); - assert - .dom('[data-test-context-option-key="move-column-to-start"]') - .exists(); - assert - .dom('[data-test-context-option-key="move-column-to-end"]') - .doesNotExist(); - }); - - test('clicking the "Move column" context menu option focuses the reorder handle', async function (assert) { - await render( - hbs``, - ); - - const thElements = findAll('.hds-advanced-table__th'); - - const firstContextMenuToggle = thElements[0].querySelector( - '.hds-dropdown-toggle-icon', - ); - await click(firstContextMenuToggle); - await click('[data-test-context-option-key="reorder-column"]'); - - const firstReorderHandle = thElements[0].querySelector( - '.hds-advanced-table__th-reorder-handle', - ); - - assert.dom(firstReorderHandle).isFocused(); - }); - - test('clicking the "Move column to start" context menu option moves the column to the start', async function (assert) { - await render( - hbs``, - ); - - const thElements = findAll('.hds-advanced-table__th'); - - const secondContextMenuToggle = thElements[1].querySelector( - '.hds-dropdown-toggle-icon', - ); - await click(secondContextMenuToggle); - await click('[data-test-context-option-key="move-column-to-start"]'); - - const columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - [this.columns[1].key, this.columns[0].key, this.columns[2].key], - 'The second column is moved to the start', - ); - }); - - test('clicking the "Move column to end" context menu option moves the column to the end', async function (assert) { - await render( - hbs``, - ); - - const thElements = findAll('.hds-advanced-table__th'); - - const secondContextMenuToggle = thElements[1].querySelector( - '.hds-dropdown-toggle-icon', - ); - await click(secondContextMenuToggle); - await click('[data-test-context-option-key="move-column-to-end"]'); - - const columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - [this.columns[0].key, this.columns[2].key, this.columns[1].key], - 'The second column is moved to the end', - ); - }); - - test('pressing "Left Arrow" and "Right Arrow" keys when the reorder handle is focused moves the column', async function (assert) { - await render( - hbs``, - ); - - const thElements = findAll('.hds-advanced-table__th'); - const firstThElement = thElements[0]; - const firstReorderHandle = thElements[0].querySelector( - '.hds-advanced-table__th-reorder-handle', - ); - await focus(firstThElement); - await focus(firstReorderHandle); - assert.dom(firstReorderHandle).isFocused(); - - await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowRight'); - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - [this.columns[1].key, this.columns[0].key, this.columns[2].key], - 'The first column is moved to the right', - ); - assert.dom(firstReorderHandle).isFocused(); - - await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowRight'); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - [this.columns[1].key, this.columns[2].key, this.columns[0].key], - 'The second column is moved to the right', - ); - assert.dom(firstReorderHandle).isFocused(); - - await triggerKeyEvent(firstReorderHandle, 'keydown', 'ArrowLeft'); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - [this.columns[1].key, this.columns[0].key, this.columns[2].key], - 'The third column is moved back to the left', - ); - assert.dom(firstReorderHandle).isFocused(); - }); - - test('passing in columnOrder sets the initial order of the table columns', async function (assert) { - await render( - hbs``, - ); - - const columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['album', 'year', 'artist'], - 'The initial column order is set correctly', - ); - }); - - test('updating columnOrder externally changes the order of the table columns', async function (assert) { - this.set('columnOrder', ['artist', 'album', 'year']); - - await render( - hbs``, - ); - - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['artist', 'album', 'year'], - 'The initial column order is set correctly', - ); - - this.set('columnOrder', ['year', 'album', 'artist']); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['year', 'album', 'artist'], - 'The column order is updated correctly', - ); - }); - - test('it throws an assertion if @hasStickyFirstColumn is true and @hasReorderableColumns is true', async function (assert) { - const errorMessage = - 'Cannot have both reorderable columns and a sticky first column.'; - - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - - await render( - hbs``, - ); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('column reordering works when there columns are added and removed dynamically', async function (assert) { - const artistColumn = { key: 'artist', label: 'Artist' }; - const albumColumn = { key: 'album', label: 'Album' }; - const yearColumn = { key: 'year', label: 'Year' }; - const genreColumn = { key: 'genre', label: 'Genre' }; - - const availableColumns = [ - artistColumn, - albumColumn, - yearColumn, - genreColumn, - ]; - - // when dealing with dynamic columns, you must handle the order of all potential columns rather than just the ones currently rendered - // inital column order is 'artist', 'album', 'year', 'genre' - const initialColumnOrder = availableColumns.map((col) => col.key); - - // initially set the columns in the reverse order to ensure the table respects the column order and ommit the genre column - const initialColumns = availableColumns - .filter((col) => col.key !== 'genre') - .reverse(); - - this.setProperties({ - columns: initialColumns, - columnOrder: initialColumnOrder, - model: this.model.map((item) => ({ ...item, genre: 'music' })), - }); - - await render( - hbs` - <:body as |B|> - - {{#each this.columns as |col|}} - {{get B.data col.key}} - {{/each}} - - - `, - ); - - // make sure the initial column order is correct based on the columnOrder - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['artist', 'album', 'year'], - 'The initial column order is set correctly', - ); - - // add the genre column and ensure it is in the correct order based on columnOrder - this.set('columns', [genreColumn, ...this.columns]); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['artist', 'album', 'year', 'genre'], - 'The column is added in the correct order based on columnOrder', - ); - - // will drop the column to the right side of the third column (year) - const expectedDropTargetIndex = 2; - const expectedDropTargetDropSide = 'right'; - - // get the first reorder handle - const firstReorderHandle = findAll( - '.hds-advanced-table__th-reorder-handle', - )[0]; - - // drag to the right side of the third column (year) - const { target, eventOptions } = await simulateColumnReorderDrag({ - handleElement: firstReorderHandle, - targetIndex: expectedDropTargetIndex, - targetPosition: expectedDropTargetDropSide, - }); - - // drop the column - await simulateColumnReorderDrop({ - target, - handleElement: firstReorderHandle, - eventOptions, - }); - - // column order updates correctly after the drag and drop - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['album', 'year', 'artist', 'genre'], - 'The initial column order is set correctly', - ); - - // remove the year column and ensure the column order is still correct - this.set( - 'columns', - this.columns.filter((col) => col.key !== 'year'), - ); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['album', 'artist', 'genre'], // album, year (hidden), artist, genre - 'The column order is correct after a column is removed', - ); - - // move the album column to the end - const albumReorderHandle = findAll( - '.hds-advanced-table__th-reorder-handle', - )[0]; - const lastIndex = this.columns.length - 1; - - const dragResult = await simulateColumnReorderDrag({ - handleElement: albumReorderHandle, - targetIndex: lastIndex, - targetPosition: 'right', - }); - - await simulateColumnReorderDrop({ - ...dragResult, - handleElement: albumReorderHandle, - }); - - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['artist', 'genre', 'album'], // year (hidden), artist, genre, album - 'The column order is correct after another column is moved', - ); - - // add the year column back and ensure it is in the correct position based on columnOrder - this.set('columns', [...this.columns, yearColumn]); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['year', 'artist', 'genre', 'album'], // year, artist, genre, album - 'The column is added back in the correct order based on columnOrder', - ); - }); - }); - - test('it should render the component with a CSS class that matches the component name', async function (assert) { - setSortableTableData(this); - - await render( - hbs``, - ); - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table'); - }); - - test('it should render with a CSS class appropriate for the @density value', async function (assert) { - setSortableTableData(this); - - await render( - hbs``, - ); - - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table--density-short'); - }); - - test('it should render with a CSS class appropriate if no @density value is set', async function (assert) { - setSortableTableData(this); - - await render( - hbs``, - ); - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table--density-medium'); - }); - - test('it should render with a CSS class appropriate for middle @valign value', async function (assert) { - setSortableTableData(this); - this.set('valign', 'middle'); - - await render( - hbs``, - ); - - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table--valign-middle'); - }); - - test('it should render with a CSS class appropriate baseline @valign value', async function (assert) { - setSortableTableData(this); - this.set('valign', 'baseline'); - - await render( - hbs``, - ); - - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table--valign-baseline'); - }); - - test('it should render with a CSS class appropriate if no @valign value is set', async function (assert) { - setSortableTableData(this); - await render( - hbs``, - ); - assert - .dom('#data-test-advanced-table [role="grid"]') - .hasClass('hds-advanced-table--valign-top'); - }); - - test('it throws an assertion if @hasReorderableColumns and has nested rows', async function (assert) { - const errorMessage = - 'Cannot have reorderable columns if there are nested rows.'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('it throws an assertion if @isStriped and has nested rows', async function (assert) { - const errorMessage = - '@isStriped must not be true if there are nested rows.'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('it throws an assertion if @hasResizableColumns and has nested rows', async function (assert) { - const errorMessage = - 'Cannot have resizable columns if there are nested rows.'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('it should support splattributes', async function (assert) { - setSortableTableData(this); - await render( - hbs``, - ); - assert - .dom('#data-test-advanced-table') - .hasAttribute('aria-label', 'data test table'); - }); - - test('it should render with a CSS class appropriate for the @hasStickyHeader argument', async function (assert) { - setSortableTableData(this); - - await render( - hbs` -<:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`, - ); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__thead') - .hasClass('hds-advanced-table__thead--sticky'); - }); - - test('it should render the appropriate CSS and add a sticky header when set @maxHeight', async function (assert) { - setSortableTableData(this); - - await render( - hbs` -<:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`, - ); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__thead') - .hasClass('hds-advanced-table__thead--sticky'); - - assert - .dom('#data-test-advanced-table .hds-advanced-table') - .hasStyle({ maxHeight: '75px' }); - }); - - test('it should render the appropriate CSS when set @maxHeight and @hasStickyHeader is set to false', async function (assert) { - setSortableTableData(this); - - await render( - hbs` -<:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`, - ); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__thead') - .doesNotHaveClass('hds-advanced-table__thead--sticky'); - - assert - .dom('#data-test-advanced-table .hds-advanced-table') - .hasStyle({ maxHeight: '75px' }); - }); - - test('it should render a table based on the data model passed', async function (assert) { - this.set('model', [ - { key: 'artist', name: 'Test 1', description: 'Test 1 description' }, - { key: 'album', name: 'Test 2', description: 'Test 2 description' }, - { key: 'year', name: 'Test 3', description: 'Test 3 description' }, - ]); - - await render(hbs` - <:body as |B|> - - {{B.data.key}} - {{B.data.name}} - {{B.data.description}} - - -`); - - assert - .dom('#data-advanced-test-table .hds-advanced-table__tr:nth-child(3)') - .hasProperty('id', '2'); - - assert - .dom( - '#data-advanced-test-table .hds-advanced-table__tr:first-of-type .hds-advanced-table__td:nth-of-type(2n)', - ) - .hasText('Test 1'); - assert - .dom( - '#data-advanced-test-table .hds-advanced-table__tr:last-of-type .hds-advanced-table__td:last-of-type', - ) - .hasText('Test 3 description'); - }); - - test('it should update the table when the model changes', async function (assert) { - const bodySelector = '.hds-advanced-table__tbody'; - const rowSelector = '.hds-advanced-table__tr'; - - setTableData(this); - await render(hbsAdvancedTable); - - assert.dom(`${bodySelector} ${rowSelector}`).exists({ count: 3 }); - assert.deepEqual(getBodyContent(), [ - ['Bob', '20', 'USA'], - ['Alice', '25', 'UK'], - ['Charlie', '30', 'Canada'], - ]); - - this.set('model', [{ name: 'Jane', age: 35, country: 'Mexico' }]); - assert.dom(`${bodySelector} ${rowSelector}`).exists({ count: 1 }); - assert.deepEqual(getBodyContent(), [['Jane', '35', 'Mexico']]); - }); - - test('it should update the table when the columns change', async function (assert) { - function getColumnLabels() { - return Array.from( - document.querySelectorAll( - '.hds-advanced-table__thead .hds-advanced-table__th', - ), - ).map((th) => th.textContent.trim()); - } - - const columns = [ - { key: 'name', label: 'Name' }, - { key: 'age', label: 'Age' }, - { key: 'country', label: 'Country' }, - ]; - - this.setProperties({ - columns, - model: [ - { name: 'Bob', age: 20, country: 'USA' }, - { name: 'Alice', age: 25, country: 'UK' }, - { name: 'Charlie', age: 30, country: 'Canada' }, - ], - }); - - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - {{B.data.country}} - - -`); - - assert.deepEqual(getColumnLabels(), ['Name', 'Age', 'Country']); - - this.set( - 'columns', - columns.map((column) => ({ - ...column, - label: `Updated ${column.label}`, - })), - ); - - assert.deepEqual(getColumnLabels(), [ - 'Updated Name', - 'Updated Age', - 'Updated Country', - ]); - }); - - // OPTIONS - - // Sortable - - test('it should render a sortable table when appropriate', async function (assert) { - setSortableTableData(this); - await render(hbsSortableAdvancedTable); - assert - .dom('#data-test-advanced-table .hds-advanced-table__th:first-of-type') - .hasClass('hds-advanced-table__th--sort'); - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__th:first-of-type .hds-advanced-table__th-content > span', - ) - .hasText('Artist'); - }); - - test('it should render a sortable table with a tooltip', async function (assert) { - setSortableTableData(this); - // add the tooltip key/value to the first column - this.columns[0].tooltip = 'More info.'; - - await render(hbsSortableAdvancedTable); - - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th:first-of-type .hds-advanced-table__th-button--tooltip', - ) - .exists(); - // activate the tooltip: - await focus( - '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th:first-of-type .hds-advanced-table__th-button--tooltip', - ); - // test that the tooltip exists and has the passed in content: - assert.dom('.tippy-content').hasText('More info.'); - }); - - test('it throws an assertion if there are selectable columns and has nested rows', async function (assert) { - const errorMessage = - 'Cannot have sortable columns if there are nested rows. Sortable columns are Name,Age'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('it throws an assertion if it has `@hasStickyFirstColumn` and has nested rows', async function (assert) { - const errorMessage = - 'Cannot have a sticky first column if there are nested rows.'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - test('it throws an assertion if it has `@hasStickyHeader` and does not have @maxHeight', async function (assert) { - const errorMessage = 'Must set @maxHeight to use @hasStickyHeader.'; - - setSortableTableData(this); - - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - - await render( - hbs` -<:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`, - ); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - // with an empty caption if no caption is provided - - test('it should render a sortable table and table is unsorted', async function (assert) { - setSortableTableData(this); - // unset the sorting applied in the `setSortableTableData` - this.set('sortBy', undefined); - this.set('sortOrder', undefined); - - await render(hbsSortableAdvancedTable); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__th:first-of-type') - .hasClass('hds-advanced-table__th--sort'); - assert - .dom('#data-test-advanced-table .hds-advanced-table__caption') - .hasText(''); - }); - - test('it updates the caption correctly after a sort has been performed', async function (assert) { - setSortableTableData(this); - // unset the sorting applied in the `setSortableTableData` - this.set('sortBy', undefined); - this.set('sortOrder', undefined); - await render(hbsSortableAdvancedTable); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('Nick Drake'); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('Melanie'); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__caption') - .hasText('Sorted by artist ascending'); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('The Beatles'); - assert - .dom('#data-test-advanced-table .hds-advanced-table__caption') - .hasText('Sorted by artist descending'); - }); - - test('it sorts the rows asc by default when the sort button is clicked on an unsorted column', async function (assert) { - setSortableTableData(this); - await render(hbsSortableAdvancedTable); - - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('Melanie'); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('The Beatles'); - }); - - test('it renders a custom sortedMessageText if supplied', async function (assert) { - setSortableTableData(this); - this.set('sortedMessageText', 'Melanie will sort it'); - - await render(hbsSortableAdvancedTable); - assert - .dom('#data-test-advanced-table .hds-advanced-table__caption') - .hasText('Melanie will sort it'); - }); - - test('it renders both a custom caption and a custom sortedMessageText if supplied', async function (assert) { - setSortableTableData(this); - this.set('caption', 'A custom caption.'); - this.set('sortedMessageText', 'Melanie will sort it!'); - - await render(hbsSortableAdvancedTable); - assert - .dom('#data-test-advanced-table .hds-advanced-table__caption') - .hasText('A custom caption. Melanie will sort it!'); - }); - - test('it uses a custom sort function if one is supplied', async function (assert) { - // contrived example; we don’t care _what_ the custom sorting function does, just that it’s used instead of the default. - // sort based on the second letter of the album name - const mySortingFunction = (a, b) => { - if (a.album.charAt(1) < b.album.charAt(1)) { - return -1; - } else if (a.album.charAt(1) > b.album.charAt(1)) { - return 1; - } else { - return 0; - } - }; - setSortableTableData(this); - this.set('columns', [ - { key: 'artist', label: 'Artist', isSortable: true }, - { - key: 'album', - label: 'Album', - isSortable: true, - sortingFunction: mySortingFunction, - }, - { key: 'year', label: 'Year' }, - ]); - - await render(hbsSortableAdvancedTable); - // let’s just check that the table is pre-sorted the way we expect (artist, ascending) - assert - .dom('#data-test-advanced-table .hds-advanced-table__td:nth-of-type(1)') - .hasText('Melanie'); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(2) button', - ); - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__td:nth-of-type(2)', - ) - .hasText('Candles in the Rain'); - }); - - test('it updates the `aria-sort` attribute value when a sort is performed', async function (assert) { - setSortableTableData(this); - await render(hbsSortableAdvancedTable); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1)', - ) - .hasAria('sort', 'descending'); - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1)', - ) - .hasAria('sort', 'ascending'); - }); - - test('it invokes the `onSort` callback when a sort is performed', async function (assert) { - let sortBy, sortOrder; - this.set('onSort', (by, ord) => { - sortBy = by; - sortOrder = ord; - }); - setSortableTableData(this); - await render(hbsSortableAdvancedTable); - - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert.strictEqual(sortBy, 'artist'); - assert.strictEqual(sortOrder, 'desc'); - await click( - '#data-test-advanced-table .hds-advanced-table__th--sort:nth-of-type(1) button', - ); - assert.strictEqual(sortBy, 'artist'); - assert.strictEqual(sortOrder, 'asc'); - }); - - test('it sorts by selected row when `@selectableColumnKey` is provided', async function (assert) { - const sortSpy = sinon.spy(); - - const sortBySelectedSelector = - '#data-test-advanced-table .hds-advanced-table__thead .hds-advanced-table__th[role="columnheader"] .hds-advanced-table__th-button--sort'; - - this.setProperties({ - model: [ - { id: 1, name: 'Bob', age: 1, isSelected: false }, - { id: 2, name: 'Sally', age: 50, isSelected: true }, - { id: 3, name: 'Jim', age: 30, isSelected: false }, - ], - selectableColumnKey: 'isSelected', - onSort: sortSpy, - }); - this.set('onSelectionChange', ({ selectionKey }) => { - const recordToUpdate = this.model.find( - (modelRow) => modelRow.id === selectionKey, - ); - if (recordToUpdate) { - recordToUpdate.isSelected = !recordToUpdate.isSelected; - } - }); - - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - -`); - - assert.dom(sortBySelectedSelector).exists(); - - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr:nth-of-type(3) .hds-advanced-table__td', - ) - .hasText('Jim'); - - await click(sortBySelectedSelector); - assert - .dom( - '#data-test-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr:nth-of-type(3) .hds-advanced-table__td', - ) - .hasText('Sally'); - - assert.ok( - sortSpy.calledWith(this.selectableColumnKey, 'asc'), - 'it invokes the `onSort` callback with the `selectableColumnKey` when a sort is performed on the selectable column', - ); - }); - - // Multi-select - - const selectAllCheckboxSelector = - '#data-test-selectable-advanced-table .hds-advanced-table__thead .hds-advanced-table__th[role="columnheader"] .hds-advanced-table__checkbox'; - const rowCheckboxesSelector = - '#data-test-selectable-advanced-table .hds-advanced-table__tbody .hds-advanced-table__th .hds-advanced-table__checkbox'; - - // basic multi-select - - test('it renders a multi-select table when isSelectable is set to true for a table with a model', async function (assert) { - setSelectableTableData(this); - await render(hbsSelectableAdvancedTable); - assert.dom(selectAllCheckboxSelector).exists({ count: 1 }); - assert.dom(rowCheckboxesSelector).exists({ count: this.model.length }); - }); - - test('it throws an assertion if @isSelectable and has nested rows', async function (assert) { - const errorMessage = - '@isSelectable must not be true if there are nested rows.'; - - setNestedTableData(this); - assert.expect(2); - setupOnerror(function (error) { - assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); - }); - await render(hbs` - <:body as |B|> - - {{B.data.name}} - {{B.data.age}} - - - `); - - assert.throws(function () { - throw new Error(errorMessage); - }); - }); - - // multi-select functionality - - test('it selects all rows when the "select all" checkbox checked state is triggered', async function (assert) { - setSelectableTableData(this); - await render(hbsSelectableAdvancedTable); - // Default should be unchecked: - assert.dom(selectAllCheckboxSelector).isNotChecked(); - assert.dom(rowCheckboxesSelector).isNotChecked().exists({ count: 3 }); - // Should change to checked after it is triggered: - await click(selectAllCheckboxSelector); - assert.dom(selectAllCheckboxSelector).isChecked(); - assert.dom(rowCheckboxesSelector).isChecked().exists({ count: 3 }); - }); - - test('it deselects all rows when the "select all" checkbox unchecked state is triggered', async function (assert) { - setSelectableTableData(this); - await render(hbsSelectableAdvancedTable); - // Trigger checked status: - await click(selectAllCheckboxSelector); - // Trigger unchecked state: - await click(selectAllCheckboxSelector); - assert.dom(selectAllCheckboxSelector).isNotChecked(); - assert.dom(rowCheckboxesSelector).isNotChecked().exists({ count: 3 }); - }); - - test('if some rows are selected but not all, the "select all" checkbox should be in an indeterminate state', async function (assert) { - setSelectableTableData(this); - await render(hbsSelectableAdvancedTable); - const rowCheckboxes = this.element.querySelectorAll(rowCheckboxesSelector); - const firstRowCheckbox = rowCheckboxes[0]; - // Check checkbox in just the first row: - await click(firstRowCheckbox); - assert.dom(selectAllCheckboxSelector).hasProperty('indeterminate', true); - }); - - test('it should invoke the `onSelectionChange` callback when a checkbox is selected', async function (assert) { - let keys; - this.set( - 'onSelectionChange', - ({ selectedRowsKeys }) => (keys = selectedRowsKeys), - ); - setSelectableTableData(this); - await render(hbs` - - <:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - - - `); - const rowCheckboxes = this.element.querySelectorAll(rowCheckboxesSelector); - const firstRowCheckbox = rowCheckboxes[0]; - await click(firstRowCheckbox); - assert.deepEqual(keys, ['1']); - await click(selectAllCheckboxSelector); - assert.deepEqual(keys, ['1', '2', '3']); - await click(selectAllCheckboxSelector); - assert.deepEqual(keys, []); - }); - - // multi-select options - - // aria-labels - - test('it renders the expected `aria-label` values for "select all" and rows by default', async function (assert) { - setSelectableTableData(this); - await render(hbs` - - <:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - - - `); - - assert.dom(selectAllCheckboxSelector).hasAria('label', 'Select all rows'); - assert.dom(rowCheckboxesSelector).hasAria('label', 'Select row'); - - await click(selectAllCheckboxSelector); - await click(rowCheckboxesSelector); - - assert.dom(selectAllCheckboxSelector).hasAria('label', 'Select all rows'); - assert.dom(rowCheckboxesSelector).hasAria('label', 'Select row'); - }); - - test('it renders the expected `aria-label` for rows with `@selectionAriaLabelSuffix`', async function (assert) { - setSelectableTableData(this); - await render(hbs` - - <:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - - - `); - - assert.dom(rowCheckboxesSelector).hasAria('label', 'Select custom suffix'); - - await click(rowCheckboxesSelector); - - assert.dom(rowCheckboxesSelector).hasAria('label', 'Select custom suffix'); - }); - - const expandRowButtonSelector = - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__th[role="rowheader"] .hds-advanced-table__th-button--expand'; - - // Nesting - - test('it renders a nested table when the model has rows with children key', async function (assert) { - setNestedTableData(this); - await render(hbsNestedAdvancedTable); - assert.dom(expandRowButtonSelector).exists({ count: 3 }); - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr', - ) - .exists({ count: 6 }); - }); - - test('it renders children rows when click the expand toggle button', async function (assert) { - setNestedTableData(this); - await render(hbsNestedAdvancedTable); - - const rowToggles = this.element.querySelectorAll(expandRowButtonSelector); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .exists({ count: 4 }); - - await click(rowToggles[0]); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .exists({ count: 2 }); - - await click(rowToggles[1]); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .exists({ count: 1 }); - }); - - test('it renders expanded children rows when pass isOpen in the model', async function (assert) { - setNestedTableData(this); - this.set('model', [ - { - id: 1, - name: 'Policy set 1', - status: 'PASS', - description: '', - isOpen: true, - children: [ - { - id: 11, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - { - id: 12, - name: 'test-hard-mandatory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - { - id: 2, - name: 'Policy set 2', - status: 'FAIL', - description: '', - isOpen: true, - children: [ - { - id: 21, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - isOpen: true, - children: [ - { - id: 211, - name: 'test-advisory-pass.sentinel.primary', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - ], - }, - ]); - await render(hbsNestedAdvancedTable); - assert.dom(expandRowButtonSelector).exists({ count: 3 }); - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr', - ) - .exists({ count: 6 }); - }); - - test('it renders an expand all button when pass isExpandable to the columns', async function (assert) { - setNestedTableData(this); - this.set('model', [ - { - id: 1, - name: 'Policy set 1', - status: 'PASS', - description: '', - isOpen: true, - children: [ - { - id: 11, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - { - id: 12, - name: 'test-hard-mandatory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - { - id: 2, - name: 'Policy set 2', - status: 'FAIL', - description: '', - children: [ - { - id: 21, - name: 'test-advisory-pass.sentinel', - status: 'PASS', - description: 'Sample description for this thing.', - children: [ - { - id: 211, - name: 'test-advisory-pass.sentinel.primary', - status: 'PASS', - description: 'Sample description for this thing.', - }, - ], - }, - ], - }, - ]); - await render(hbsNestedAdvancedTable); - - const expandAllButton = document.querySelector( - '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', - ); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', - ) - .exists({ count: 1 }); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .exists({ count: 2 }); - assert.dom(expandAllButton).hasAria('expanded', 'false'); - - await click(expandAllButton); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .doesNotExist(); - assert.dom(expandAllButton).hasAria('expanded', 'true'); - - await click(expandAllButton); - - assert - .dom( - '#data-test-nested-advanced-table .hds-advanced-table__tbody .hds-advanced-table__tr.hds-advanced-table__tr--hidden', - ) - .exists({ count: 4 }); - assert.dom(expandAllButton).hasAria('expanded', 'false'); - }); - - test('the expand all button state updates when expand buttons are clicked', async function (assert) { - setNestedTableData(this); - await render(hbsNestedAdvancedTable); - - const rowToggles = Array.from( - this.element.querySelectorAll(expandRowButtonSelector), - ); - const expandAllButton = document.querySelector( - '#data-test-nested-advanced-table .hds-advanced-table__thead .hds-advanced-table__th .hds-advanced-table__th-button--expand', - ); - - assert.dom(expandAllButton).hasAria('expanded', 'false'); - - for (let i = 0; i < rowToggles.length; i++) { - await click(rowToggles[i]); - - if (i < rowToggles.length - 1) { - assert.dom(expandAllButton).hasAria('expanded', 'false'); - } - } - - assert.dom(expandAllButton).hasAria('expanded', 'true'); - }); - - // Resizing - - test('it should allow resizing columns with the resize handle (pointer events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - assert - .dom('.hds-advanced-table__th-resize-handle') - .exists({ count: 1 }, 'There is one resize handle (not on last column)'); - - const handle = find('.hds-advanced-table__th-resize-handle'); // get the first handle - - // Simulate pointer drag to the right (increase width) - await simulateRightPointerDrag(handle); - - const newGridValues = getTableGridValues(table); - assert.notEqual( - newGridValues, - originalGridValues, - 'Grid values changed after drag', - ); - }); - - test('it should allow resizing columns with the resize handle (keyboard events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Focus and send ArrowRight key - await focus(handle); - await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); - - let newGridValues = getTableGridValues(table); - - assert.notOk( - gridValuesAreEqual(originalGridValues, newGridValues), - 'Grid values are not equal after ArrowRight', - ); - - // Send ArrowLeft key - await triggerKeyEvent(handle, 'keydown', 'ArrowLeft'); - - newGridValues = getTableGridValues(table); - - assert.ok( - gridValuesAreEqual(originalGridValues, newGridValues), - 'Grid values are equal after ArrowLeft', - ); - }); - - test('it should not allow resizing columns below their minimum width (pointer events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Try to resize column to a very small width (well below minWidth of 60px) - await triggerEvent(handle, 'pointerdown', { clientX: 100 }); - await triggerEvent(window, 'pointermove', { clientX: 1 }); - await triggerEvent(window, 'pointerup'); - - const newGridValues = getTableGridValues(table); - assert.notEqual( - newGridValues, - originalGridValues, - 'Grid values changed after pointer drag', - ); - - const firstColumnGridValue = newGridValues[0]; - - assert.ok( - parseInt(firstColumnGridValue, 10) >= 60, - `Column width respects minimum width constraint (actual: ${firstColumnGridValue}, min: 60px)`, - ); - }); - - test('it should not allow resizing columns above their maximum width (pointer events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Try to resize column to a very large width (well below minWidth of 60px) - await triggerEvent(handle, 'pointerdown', { clientX: 100 }); - await triggerEvent(window, 'pointermove', { clientX: 10000 }); - await triggerEvent(window, 'pointerup'); - - // Check the new width - const newGridValues = getTableGridValues(table); - assert.notEqual( - newGridValues, - originalGridValues, - 'Grid values changed after pointer drag', - ); - - const firstColumnGridValue = newGridValues[0]; - - assert.ok( - parseInt(firstColumnGridValue, 10) <= 300, - `Column width respects maximum width constraint (actual: ${firstColumnGridValue}px, max: 300px)`, - ); - }); - - test('it should not allow resizing columns below their minimum width (keyboard events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Focus handle and press ArrowLeft multiple times to try going below min width - await focus(handle); - - for (let i = 0; i < 10; i++) { - // moves left 10px each time - await triggerKeyEvent(handle, 'keydown', 'ArrowLeft'); - } - - const newGridValues = getTableGridValues(table); - assert.notEqual( - newGridValues, - originalGridValues, - 'Grid values changed after ArrowLeft', - ); - - const firstColumnGridValue = newGridValues[0]; - - assert.ok( - parseInt(firstColumnGridValue, 10) >= 60, - `Column width respects minimum width constraint with keyboard events (actual: ${firstColumnGridValue}, min: 60px)`, - ); - }); - - test('it should not allow resizing columns above their maximum width (keyboard events)', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Focus handle and press ArrowLeft multiple times to try going below min width - await focus(handle); - - for (let i = 0; i < 10; i++) { - // moves right 10px each time - await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); - } - - const newGridValues = getTableGridValues(table); - assert.notEqual( - newGridValues, - originalGridValues, - 'Grid values changed after ArrowRight', - ); - - const firstColumnGridValue = newGridValues[0]; - - assert.ok( - parseInt(firstColumnGridValue, 10) <= 300, - `Column width respects maximum width constraint with keyboard events (actual: ${firstColumnGridValue}px, max: 300px)`, - ); - }); - - test('it should show the context menu when resizing is enabled', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const th = find('.hds-advanced-table__th'); // find the first header cell - - assert.ok( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - await click(contextMenuToggle); - - assert.dom('[data-test-context-option-key="reset-column-width"]').exists(); - }); - - test('it should resize the column to the initial width when resetting column width', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const table = find('.hds-advanced-table'); - const originalGridValues = getTableGridValues(table); - - const handle = find('.hds-advanced-table__th-resize-handle'); - const th = handle.closest('.hds-advanced-table__th'); - - await simulateRightPointerDrag(handle); - - let newGridValues = getTableGridValues(table); - - assert.notOk( - gridValuesAreEqual(originalGridValues, newGridValues), - 'Grid values are not equal after resizing', - ); - - await performContextMenuAction(th, 'reset-column-width'); - - newGridValues = getTableGridValues(table); - assert.ok( - gridValuesAreEqual(originalGridValues, newGridValues), - 'Grid values reset to initial state after resetting column width', - ); - }); - - test('it should focus the resize handle when the "resize column" context menu option is clicked', async function (assert) { - setResizableColumnsTableData(this); - await render(hbsResizableColumnsAdvancedTable); - - const handle = find('.hds-advanced-table__th-resize-handle'); - const th = handle.closest('.hds-advanced-table__th'); - - await performContextMenuAction(th, 'resize-column'); - - assert.ok(handle === document.activeElement, 'Resize handle is focused'); - }); - - test('it should call `onColumnResize` when a column is resized by dragging', async function (assert) { - setResizableColumnsTableData(this); - const onColumnResizeSpy = sinon.spy(); - this.set('onColumnResize', onColumnResizeSpy); - - await render(hbs` - - <:body as |B|> - - {{B.data.col1}} - {{B.data.col2}} - - - - `); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - await focus(handle); - - await triggerKeyEvent(handle, 'keydown', 'ArrowRight'); - - assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); - }); - - test('it should call `onColumnResize` when a column is resized by keyboard', async function (assert) { - setResizableColumnsTableData(this); - const onColumnResizeSpy = sinon.spy(); - this.set('onColumnResize', onColumnResizeSpy); - - await render(hbs` - - <:body as |B|> - - {{B.data.col1}} - {{B.data.col2}} - - - - `); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - // Simulate pointer drag to the right (increase width) - await simulateRightPointerDrag(handle); - - assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); - }); - - test('it should call `onColumnResize` when a column width is reset', async function (assert) { - setResizableColumnsTableData(this); - const onColumnResizeSpy = sinon.spy((key) => { - console.log('Column resized', key); - }); - this.set('onColumnResize', onColumnResizeSpy); - - await render(hbs` - - <:body as |B|> - - {{B.data.col1}} - {{B.data.col2}} - - - - `); - - const handle = find('.hds-advanced-table__th-resize-handle'); - - await simulateRightPointerDrag(handle); - - assert.ok(onColumnResizeSpy.calledOnce, 'onColumnResize was called'); - - await performContextMenuAction( - handle.closest('.hds-advanced-table__th'), - 'reset-column-width', - ); - assert.ok( - onColumnResizeSpy.calledTwice, - 'onColumnResize was called again after resetting column width', - ); - }); - - // Sticky Columns - - test('it should render with a CSS class appropriate for the @hasStickyFirstColumn argument', async function (assert) { - setSortableTableData(this); - - await render( - hbs` -<:body as |B|> - - {{B.data.artist}} - {{B.data.album}} - {{B.data.year}} - - -`, - ); - - assert - .dom( - '.hds-advanced-table__th--sort.hds-advanced-table__th--is-sticky-column', - ) - .exists({ count: 1 }); - - assert - .dom( - '.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column:not(.hds-advanced-table__th--sort)', - ) - .exists({ count: 3 }); - }); - - test('it should render with a CSS class appropriate for the @hasStickyFirstColumn argument when also selectable', async function (assert) { - setSelectableTableData(this); - this.set('hasStickyFirstColumn', true); - await render(hbsSelectableAdvancedTable); - - assert - .dom( - '.hds-advanced-table__th--is-selectable.hds-advanced-table__th--is-sticky-column', - ) - .exists({ count: 4 }); - - assert - .dom( - '.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column:not(.hds-advanced-table__th--is-selectable)', - ) - .exists({ count: 4 }); - }); - - test('it should show the context menu when the @hasStickyFirstColumn argument is true', async function (assert) { - setTableData(this); - this.set('hasStickyFirstColumn', true); - await render(hbsAdvancedTable); - - const th = find('.hds-advanced-table__th'); // find the first header cell - - assert.ok( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - await click(contextMenuToggle); - - assert.dom('[data-test-context-option-key="pin-first-column"]').exists(); - }); - - test('it should show the context menu when the @hasStickyFirstColumn argument is false', async function (assert) { - setTableData(this); - this.set('hasStickyFirstColumn', false); - await render(hbsAdvancedTable); - - const th = find('.hds-advanced-table__th'); // find the first header cell - - assert.ok( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - await click(contextMenuToggle); - - assert.dom('[data-test-context-option-key="pin-first-column"]').exists(); - }); - - test('it should not show the context menu when the @hasStickyFirstColumn argument is undefined', async function (assert) { - setTableData(this); - await render(hbsAdvancedTable); - - const th = find('.hds-advanced-table__th'); // find the first header cell - - assert.notOk( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - }); - - test('it should toggle column pinning when the context menu item is clicked', async function (assert) { - setTableData(this); - this.set('hasStickyFirstColumn', false); - await render(hbsAdvancedTable); - - const th = find('.hds-advanced-table__th'); // find the first header cell - - // Pin column - await performContextMenuAction(th, 'pin-first-column'); - - assert - .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') - .exists({ count: 1 }); - - // Unpin column - await performContextMenuAction(th, 'pin-first-column'); - - assert - .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') - .doesNotExist(); - }); - - test('it should show the context menu when the @hasStickyFirstColumn argument is true and the column is sortable', async function (assert) { - setSortableTableData(this); - this.set('hasStickyFirstColumn', true); - await render(hbsSortableAdvancedTable); - - const th = find('.hds-advanced-table__th--sort'); // find the first header cell - - assert.ok( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - await click(contextMenuToggle); - - assert.dom('[data-test-context-option-key="pin-first-column"]').exists(); - }); - - test('it should show the context menu when the @hasStickyFirstColumn argument is false and the column is sortable', async function (assert) { - setSortableTableData(this); - this.set('hasStickyFirstColumn', false); - await render(hbsSortableAdvancedTable); - - const th = find('.hds-advanced-table__th--sort'); // find the first header cell - - assert.ok( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - - const contextMenuToggle = th.querySelector('.hds-dropdown-toggle-icon'); - await click(contextMenuToggle); - - assert.dom('[data-test-context-option-key="pin-first-column"]').exists(); - }); - - test('it should not show the context menu when the @hasStickyFirstColumn argument is undefined', async function (assert) { - setSortableTableData(this); - await render(hbsSortableAdvancedTable); - - const th = find('.hds-advanced-table__th--sort'); // find the first header cell - - assert.notOk( - th.querySelector('.hds-advanced-table__th-context-menu'), - 'context menu exists', - ); - }); - - test('it should toggle column pinning when the context menu item is clicked and the column is sortable', async function (assert) { - setSortableTableData(this); - this.set('hasStickyFirstColumn', false); - await render(hbsSortableAdvancedTable); - - const th = find('.hds-advanced-table__th--sort'); // find the first header cell - - // Pin column - await performContextMenuAction(th, 'pin-first-column'); - - assert - .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') - .exists({ count: 1 }); - - // Unpin column - await performContextMenuAction(th, 'pin-first-column'); - - assert - .dom('.hds-advanced-table__th.hds-advanced-table__th--is-sticky-column') - .doesNotExist(); - }); - - // Resize behavior tests - test('columns will grow to fill available space when width is not explicitly set', async function (assert) { - this.set('width', '300px'); - - await render(hbs` -
- - <:body as |B|> - - {{B.data.name}} - {{B.data.biography}} - {{B.data.occupation}} - {{B.data.age}} - {{B.data.hair}} - {{B.data.eyes}} - {{B.data.salary}} - - - -
- `); - - // eslint-disable-next-line ember/no-settled-after-test-helper - await settled(); - - const table = find('#data-test-advanced-table'); - const container = find('#resize-test-container'); - - assert.ok( - table.offsetWidth >= container.offsetWidth, - 'Table width is greater than the container width', - ); - - this.set('width', '100%'); - - await settled(); - - assert.ok( - table.offsetWidth === container.offsetWidth, - 'Table width grows to fit container width', - ); - }); - - test('it should render correct columns when columns are added or removed dynamically', async function (assert) { - setTableData(this); - - const columns = [ - { key: 'name', label: 'Name' }, - { key: 'age', label: 'Age' }, - { key: 'country', label: 'Country' }, - ]; - const bodyContent = [ - ['Bob', '20', 'USA'], - ['Alice', '25', 'UK'], - ['Charlie', '30', 'Canada'], - ]; - - this.set('columns', columns); - - await render(hbs` - <:body as |B|> - - {{#each this.columns as |column|}} - {{get B.data column.key}} - {{/each}} - - -`); - - let columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['name', 'age', 'country'], - 'Initial columns are correct', - ); - assert.deepEqual(getBodyContent(), bodyContent); - - this.set( - 'columns', - this.columns.filter((col) => col.key !== 'age'), - ); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['name', 'country'], - 'Columns are correct after removing age', - ); - assert.deepEqual(getBodyContent(), [ - ['Bob', 'USA'], - ['Alice', 'UK'], - ['Charlie', 'Canada'], - ]); - - this.set('columns', columns); - columnOrder = await getColumnOrder(this.columns); - assert.deepEqual( - columnOrder, - ['name', 'age', 'country'], - 'Columns are correct after adding age back', - ); - assert.deepEqual(getBodyContent(), bodyContent); - }); -}); diff --git a/showcase/tests/integration/components/hds/advanced-table/td-test.js b/showcase/tests/integration/components/hds/advanced-table/td-test.gts similarity index 71% rename from showcase/tests/integration/components/hds/advanced-table/td-test.js rename to showcase/tests/integration/components/hds/advanced-table/td-test.gts index 490dd38c43c..5dd5e8026be 100644 --- a/showcase/tests/integration/components/hds/advanced-table/td-test.js +++ b/showcase/tests/integration/components/hds/advanced-table/td-test.gts @@ -4,16 +4,20 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAdvancedTableTd } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/advanced-table/td', function (hooks) { setupRenderingTest(hooks); test('it renders with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#data-test-advanced-table-td') @@ -22,14 +26,18 @@ module('Integration | Component | hds/advanced-table/td', function (hooks) { test('it should render with the appropriate role', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-td').hasAttribute('role', 'gridcell'); }); test('it should render with the appropriate `@align` CSS class', async function (assert) { await render( - hbs``, + , ); assert .dom('#data-test-advanced-table-td') @@ -38,7 +46,9 @@ module('Integration | Component | hds/advanced-table/td', function (hooks) { test('it should render with the appropriate span information by default', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-td').hasNoAttribute('aria-rowspan'); @@ -51,11 +61,13 @@ module('Integration | Component | hds/advanced-table/td', function (hooks) { test('it should render with the appropriate span information when pass rowspan and colspan', async function (assert) { await render( - hbs``, + , ); assert @@ -74,7 +86,9 @@ module('Integration | Component | hds/advanced-table/td', function (hooks) { test('it should support splattributes', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-td').hasAttribute('lang', 'es'); }); diff --git a/showcase/tests/integration/components/hds/advanced-table/th-sort-test.js b/showcase/tests/integration/components/hds/advanced-table/th-sort-test.gts similarity index 62% rename from showcase/tests/integration/components/hds/advanced-table/th-sort-test.js rename to showcase/tests/integration/components/hds/advanced-table/th-sort-test.gts index 78d759adffe..3462628a45e 100644 --- a/showcase/tests/integration/components/hds/advanced-table/th-sort-test.js +++ b/showcase/tests/integration/components/hds/advanced-table/th-sort-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; +import { render, click, focus, setupOnerror, find } from '@ember/test-helpers'; + +import { HdsAdvancedTableThSort } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, click, focus, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module( 'Integration | Component | hds/advanced-table/th-sort', @@ -14,7 +16,11 @@ module( setupRenderingTest(hooks); test('it renders with a CSS class that matches the component name', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -31,7 +37,11 @@ module( test('it renders text content yielded within the cell', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom( @@ -44,7 +54,14 @@ module( test('it should render with the appropriate `@align` CSS class', async function (assert) { await render( - hbs`Year`, + , ); assert .dom('#data-test-advanced-table-th-sort') @@ -55,7 +72,9 @@ module( test('if @sortOrder is not defined, the swap-vertical icon should be displayed', async function (assert) { await render( - hbs`Artist`, + , ); assert.dom('[data-test-icon="swap-vertical"]').exists(); @@ -63,13 +82,21 @@ module( test('if sorted, and `@sortOrder` is set, the correct icon should be displayed', async function (assert) { await render( - hbs`Artist`, + , ); assert.dom('[data-test-icon="arrow-up"]').exists(); await render( - hbs`Artist`, + , ); assert.dom('[data-test-icon="arrow-down"]').exists(); @@ -79,14 +106,20 @@ module( test('it should support splattributes', async function (assert) { await render( - hbs`Artist`, + , ); assert.dom('#data-test-advanced-table-th').hasAttribute('lang', 'es'); }); test('it has a role and it is set to columnheader by default', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -95,7 +128,9 @@ module( }); test('the default `scope` attribute can not be overwritten', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -105,7 +140,11 @@ module( test('if unsorted, the aria-sort attribute value should be set to none', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -114,7 +153,14 @@ module( }); test('if sorted, the aria-sort attribute value should reflect the direction', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -123,15 +169,22 @@ module( }); test('it renders the `aria-labelledby` attribute for the sort button with the correct IDs', async function (assert) { await render( - hbs`Artist`, + , ); - const prefixLabel = this.element.querySelector( + const prefixLabel = find( '#data-test-advanced-table-th-sort .hds-advanced-table__th-button-aria-label-hidden-segment:nth-of-type(1)', ); - const buttonLabel = this.element.querySelector( + const buttonLabel = find( '#data-test-advanced-table-th-sort .hds-advanced-table__th-content > span', ); - const suffixLabel = this.element.querySelector( + const suffixLabel = find( '#data-test-advanced-table-th-sort .hds-advanced-table__th-button-aria-label-hidden-segment:nth-of-type(2)', ); assert @@ -140,7 +193,7 @@ module( ) .hasAria( 'labelledby', - `${prefixLabel.id} ${buttonLabel.id} ${suffixLabel.id}`, + `${prefixLabel?.id} ${buttonLabel?.id} ${suffixLabel?.id}`, ); assert.dom(suffixLabel).hasText('ascending'); }); @@ -154,11 +207,13 @@ module( }); await render( - hbs``, + , ); assert.throws(function () { @@ -176,11 +231,13 @@ module( }); await render( - hbs``, + , ); assert.throws(function () { @@ -192,9 +249,19 @@ module( test('it should call the `@onClickSort` function if provided', async function (assert) { let isClicked = false; - this.set('onClickSort', () => (isClicked = true)); + const onClickSort = () => { + isClicked = true; + }; + await render( - hbs`Artist`, + , ); await click( '#data-test-advanced-table-th-sort .hds-advanced-table__th-button--sort', @@ -206,7 +273,11 @@ module( test('if @tooltip is undefined a tooltip button toggle should not be present', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -217,7 +288,14 @@ module( }); test('if @tooltip is defined a tooltip should be added to the table cell header', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -234,19 +312,26 @@ module( }); test('it renders the `aria-labelledby` attribute for the tooltip button with the correct IDs', async function (assert) { await render( - hbs`Artist`, + , ); - let prefixLabel = this.element.querySelector( + const prefixLabel = find( '#data-test-advanced-table-th-sort .hds-advanced-table__th-button-aria-label-hidden-segment', ); - let buttonLabel = this.element.querySelector( + const buttonLabel = find( '#data-test-advanced-table-th-sort .hds-advanced-table__th-content > span', ); assert .dom( '#data-test-advanced-table-th-sort .hds-advanced-table__th-button--tooltip', ) - .hasAria('labelledby', `${prefixLabel.id} ${buttonLabel.id}`); + .hasAria('labelledby', `${prefixLabel?.id} ${buttonLabel?.id}`); }); }, ); diff --git a/showcase/tests/integration/components/hds/advanced-table/th-test.js b/showcase/tests/integration/components/hds/advanced-table/th-test.gts similarity index 64% rename from showcase/tests/integration/components/hds/advanced-table/th-test.js rename to showcase/tests/integration/components/hds/advanced-table/th-test.gts index 69048fe5486..efc81ad6277 100644 --- a/showcase/tests/integration/components/hds/advanced-table/th-test.js +++ b/showcase/tests/integration/components/hds/advanced-table/th-test.gts @@ -4,18 +4,25 @@ */ import { module, test } from 'qunit'; +import { render, focus, click, setupOnerror, find } from '@ember/test-helpers'; + +import { + HdsAdvancedTable, + HdsAdvancedTableTh, +} from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, focus, click, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/advanced-table/th', function (hooks) { setupRenderingTest(hooks); test('it should render with a CSS class that matches the component name', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -27,9 +34,11 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it renders text content yielded within the cell (no tooltip)', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom( @@ -40,10 +49,14 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it renders text content yielded within the cell (with tooltip)', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom( @@ -56,9 +69,11 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it does not render an expand button by default', async function (assert) { await render( - hbs`Artist`, + , ); assert.dom('.hds-advanced-table__th-button--expand').doesNotExist(); @@ -66,10 +81,14 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it renders an expand button when `@isExpandable` is true and defaults to collapsed', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom( @@ -81,11 +100,15 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it renders an expand button when `@isExpandable` is true and is expanded if `@isExpanded`', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom( @@ -99,14 +122,20 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it should call the `@onClickToggle` function if provided', async function (assert) { let isClicked = false; - this.set('onClickToggle', () => (isClicked = true)); + const onClickToggle = () => { + isClicked = true; + }; await render( - hbs`Artist`, + , ); await click( @@ -120,10 +149,11 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it should render with the appropriate `@align` CSS class', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom('#data-advanced-test-table-th') @@ -134,7 +164,9 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it should render with the appropriate span information by default', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-th').hasNoAttribute('aria-rowspan'); @@ -147,11 +179,13 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it should render with the appropriate span information when pass rowspan and colspan', async function (assert) { await render( - hbs``, + , ); assert @@ -177,11 +211,13 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { }); await render( - hbs``, + , ); assert.throws(function () { @@ -199,11 +235,13 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { }); await render( - hbs``, + , ); assert.throws(function () { @@ -213,19 +251,22 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('it should support splattributes', async function (assert) { await render( - hbs`Artist`, + , ); assert.dom('#data-advanced-test-table-th').hasAttribute('lang', 'es'); }); test('it has the role attribute set to columnheader by default', async function (assert) { await render( - hbs`Artist`, + , ); assert .dom('#data-advanced-test-table-th') @@ -233,24 +274,28 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { }); test('it has the role rowheader if inside a tbody', async function (assert) { - this.set('model', [ + const model = [ { key: 'artist', name: 'Test 1', description: 'Test 1 description' }, { key: 'album', name: 'Test 2', description: 'Test 2 description' }, { key: 'year', name: 'Test 3', description: 'Test 3 description' }, - ]); + ]; - this.set('columns', [ + const columns = [ { key: 'artist', label: 'components.table.headers.artist' }, { key: 'album', label: 'components.table.headers.album' }, { key: 'year', label: 'components.table.headers.year' }, - ]); + ]; await render( - hbs`<:body - as |B| - >Artist`, + , ); assert .dom('#data-advanced-test-table-th') @@ -261,9 +306,11 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { test('if @tooltip is undefined a tooltip button toggle should not be present', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -274,10 +321,14 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { }); test('if @tooltip is defined a tooltip should be added to the table cell header', async function (assert) { await render( - hbs`Artist`, + , ); assert @@ -294,18 +345,25 @@ module('Integration | Component | hds/advanced-table/th', function (hooks) { }); test('it renders the `aria-labelledby` attribute for the tooltip button with the correct IDs', async function (assert) { await render( - hbs`Artist`, + , ); - let prefixLabel = this.element.querySelector( + const prefixLabel = find( '#data-advanced-test-table-th .hds-advanced-table__th-button-aria-label-hidden-segment', ); - let buttonLabel = this.element.querySelector( + const buttonLabel = find( '#data-advanced-test-table-th .hds-advanced-table__th-content > span', ); assert .dom( '#data-advanced-test-table-th .hds-advanced-table__th-button--tooltip', ) - .hasAria('labelledby', `${prefixLabel.id} ${buttonLabel.id}`); + .hasAria('labelledby', `${prefixLabel?.id} ${buttonLabel?.id}`); }); }); diff --git a/showcase/tests/integration/components/hds/advanced-table/tr-test.js b/showcase/tests/integration/components/hds/advanced-table/tr-test.gts similarity index 59% rename from showcase/tests/integration/components/hds/advanced-table/tr-test.js rename to showcase/tests/integration/components/hds/advanced-table/tr-test.gts index b31291c43f8..97c375df564 100644 --- a/showcase/tests/integration/components/hds/advanced-table/tr-test.js +++ b/showcase/tests/integration/components/hds/advanced-table/tr-test.gts @@ -4,16 +4,22 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, click, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsAdvancedTableTr } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; +import NOOP from 'showcase/utils/noop'; module('Integration | Component | hds/advanced-table/tr', function (hooks) { setupRenderingTest(hooks); test('it should render with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert @@ -23,7 +29,9 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { test('it should render with the appropriate role', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-tr').hasAttribute('role', 'row'); }); @@ -32,8 +40,10 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { test('it should render the yielded content', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-tr > td').exists(); }); @@ -45,50 +55,60 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { test('it should not render a checkbox if `@isSelectable` is not set', async function (assert) { await render( - hbs``, + , ); assert.dom(checkboxSelector).doesNotExist(); }); test('it should render a checkbox if `@isSelectable` is `true`', async function (assert) { await render( - hbs``, + , ); assert.dom(checkboxSelector).exists(); }); test('the checkbox should be checked if `@isSelected` is `true`', async function (assert) { await render( - hbs``, + , ); assert.dom(checkboxSelector).isChecked(); }); test('the checkbox contains the `@selectionAriaLabelSuffix` suffix', async function (assert) { await render( - hbs``, + , ); assert.dom(checkboxSelector).hasAria('label', 'Select row 123'); }); test('the `th` element has the correct `role` attribute value provided via `@selectionScope`', async function (assert) { await render( - hbs``, + , ); assert .dom( @@ -98,33 +118,41 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { }); test('it should invoke the `onSelectionChange` callback when the checkbox is selected', async function (assert) { - let key; - this.set( - 'onSelectionChange', - (_checkbox, selectionKey) => (key = selectionKey), - ); + const context = new TrackedObject<{ key?: string }>({ + key: undefined, + }); + + const onSelectionChange = ( + _checkbox?: HTMLInputElement, + selectionKey?: string, + ) => { + context.key = selectionKey; + }; + await render( - hbs``, + , ); await click(checkboxSelector); - assert.strictEqual(key, 'row123'); + assert.strictEqual(context.key, 'row123'); }); test('it should render a sort button in the checkbox cell if `@onClickSortBySelected` is provided and `@isSelectable` is `true`', async function (assert) { - this.set('noop', () => {}); - await render( - hbs``, + , ); assert @@ -134,10 +162,12 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { test('it should not render a sort button in the checkbox cell if `@isSelectable` is `true`, and `@onClickSortBySelected` is undefined', async function (assert) { await render( - hbs``, + , ); assert @@ -149,7 +179,9 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { test('it should support splattributes', async function (assert) { await render( - hbs``, + , ); assert.dom('#data-test-advanced-table-tr').hasAttribute('lang', 'es'); }); @@ -164,7 +196,9 @@ module('Integration | Component | hds/advanced-table/tr', function (hooks) { assert.strictEqual(error.message, errorMessage); }); await render( - hbs``, + , ); assert.throws(function () { throw new Error(errorMessage); diff --git a/showcase/tests/integration/components/hds/alert/index-test.js b/showcase/tests/integration/components/hds/alert/index-test.gts similarity index 64% rename from showcase/tests/integration/components/hds/alert/index-test.js rename to showcase/tests/integration/components/hds/alert/index-test.gts index d418865a48e..40b6346b96c 100644 --- a/showcase/tests/integration/components/hds/alert/index-test.js +++ b/showcase/tests/integration/components/hds/alert/index-test.gts @@ -4,9 +4,12 @@ */ import { module, test } from 'qunit'; +import { render, resetOnerror, setupOnerror, find } from '@ember/test-helpers'; + +import { HdsAlert } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import NOOP from 'showcase/utils/noop'; module('Integration | Component | hds/alert/index', function (hooks) { setupRenderingTest(hooks); @@ -16,14 +19,18 @@ module('Integration | Component | hds/alert/index', function (hooks) { }); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-alert').hasClass('hds-alert'); }); // TYPE test('it should render the correct CSS type class depending on the @type prop', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-alert').hasClass('hds-alert--type-page'); }); @@ -31,29 +38,43 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render an icon by default depending on the type and color', async function (assert) { // here we don't test all the possible combinations, only some of them as precaution - await render(hbs``); + await render(); assert.dom('.hds-icon-info').exists(); - await render(hbs``); + await render(); assert.dom('.hds-icon-info-fill').exists(); - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-info').exists(); - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-check-circle').exists(); - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-alert-triangle').exists(); - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-alert-diamond').exists(); }); test('if an icon is declared, the icon should render in the component and override the default one', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-clipboard-copy').exists(); - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon-clipboard-copy').exists(); }); test('it should display no icon when @icon is set to false', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('.hds-icon').doesNotExist(); }); @@ -61,19 +82,32 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render the title when the "title" contextual component is provided', async function (assert) { await render( - hbs`This is the title`, + , ); - assert.dom(this.element).hasText('This is the title'); + assert.dom('.hds-alert').hasText('This is the title'); }); test('it should render the description when the "description" contextual component is provided', async function (assert) { await render( - hbs`This is the description`, + , ); - assert.dom(this.element).hasText('This is the description'); + assert.dom('.hds-alert').hasText('This is the description'); }); test('it should render rich HTML when the "description" contextual component contains HTML tags', async function (assert) { await render( - hbs`Hello strong and em and code and link`, + , ); assert.dom('.hds-alert__description strong').exists().hasText('strong'); assert.dom('.hds-alert__description em').exists().hasText('em'); @@ -81,17 +115,23 @@ module('Integration | Component | hds/alert/index', function (hooks) { assert.dom('.hds-alert__description a').exists().hasText('link'); }); test('it should render a div when the @tag argument is not provided', async function (assert) { - await render(hbs` - - This is the title - `); + await render( + , + ); assert.dom('.hds-alert__title').hasTagName('div'); }); test('it should render the custom title tag when the @tag argument is provided', async function (assert) { - await render(hbs` - - This is the title - `); + await render( + , + ); assert.dom('.hds-alert__title').hasTagName('h2'); }); @@ -99,7 +139,19 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render an Hds::Button component yielded to the "actions" container', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-alert .hds-alert__actions button') @@ -111,7 +163,21 @@ module('Integration | Component | hds/alert/index', function (hooks) { }); test('it should render an Hds::Link::Standalone component yielded to the "actions" container', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-alert .hds-alert__actions a') @@ -126,7 +192,10 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render any content passed to the "generic" contextual component', async function (assert) { await render( - hbs`
test
`, + , ); assert.dom('#test-alert .hds-alert__content pre').exists().hasText('test'); }); @@ -134,12 +203,13 @@ module('Integration | Component | hds/alert/index', function (hooks) { // DISMISS test('it should not render the "dismiss" button by default', async function (assert) { - await render(hbs``); + await render(); assert.dom('button.hds-alert__dismiss').doesNotExist(); }); test('it should render the "dismiss" button if a callback function is passed to the @onDismiss argument', async function (assert) { - this.set('NOOP', () => {}); - await render(hbs``); + await render( + , + ); assert.dom('button.hds-alert__dismiss').exists(); }); @@ -149,7 +219,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render the component with role="alert" and aria-live="polite" for the "success" color', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-alert').hasAttribute('role', 'alert'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -157,7 +229,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render the component with role="alert" and aria-live="polite" for the "warning" color', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-alert').hasAttribute('role', 'alert'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -165,7 +239,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render the component with role="alert" and aria-live="polite" for the "critical" color', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-alert').hasAttribute('role', 'alert'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -175,7 +251,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should not render the component with role="alert" and aria-live="polite" for the "neutral" color', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-alert').doesNotHaveAttribute('role', 'alert'); assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite'); @@ -183,7 +261,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should not render the component with role="alert" and aria-live="polite" for the "highlight" color', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-alert').doesNotHaveAttribute('role', 'alert'); assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite'); @@ -193,28 +273,28 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render with an auto-generated `aria-labelledby` when a title is provided', async function (assert) { await render( - hbs` - + , ); - let title = this.element.querySelector('#test-alert .hds-alert__title'); - assert.dom('#test-alert').hasAttribute('aria-labelledby', title.id); + const title = find('#test-alert .hds-alert__title'); + assert.dom('#test-alert').hasAttribute('aria-labelledby', title?.id ?? ''); }); test('it should render with an auto-generated `aria-labelledby` when description is provided', async function (assert) { await render( - hbs` - + , ); - let description = this.element.querySelector( - '#test-alert .hds-alert__description', - ); - assert.dom('#test-alert').hasAttribute('aria-labelledby', description.id); + const description = find('#test-alert .hds-alert__description'); + assert + .dom('#test-alert') + .hasAttribute('aria-labelledby', description?.id ?? ''); }); // Alert dialogs @@ -223,11 +303,11 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render with with role="alertdialog" and aria-live="polite" for the "success" color when actions are provided', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-alert').hasAttribute('role', 'alertdialog'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -235,11 +315,11 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render with with role="alertdialog" and aria-live="polite" for the "warning" color when actions are provided', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-alert').hasAttribute('role', 'alertdialog'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -247,11 +327,11 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should render with with role="alertdialog" and aria-live="polite" for the "critical" color when actions are provided', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-alert').hasAttribute('role', 'alertdialog'); assert.dom('#test-alert').hasAttribute('aria-live', 'polite'); @@ -261,11 +341,11 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should not render with with role="alertdialog" and aria-live="polite" for the "neutral" color when actions are provided', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-alert').doesNotHaveAttribute('role', 'alertdialog'); assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite'); @@ -273,11 +353,11 @@ module('Integration | Component | hds/alert/index', function (hooks) { test('it should not render with with role="alertdialog" and aria-live="polite" for the "highlight" color when actions are provided', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-alert').doesNotHaveAttribute('role', 'alertdialog'); assert.dom('#test-alert').doesNotHaveAttribute('aria-live', 'polite'); @@ -292,7 +372,12 @@ module('Integration | Component | hds/alert/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); @@ -304,7 +389,9 @@ module('Integration | Component | hds/alert/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); @@ -316,7 +403,12 @@ module('Integration | Component | hds/alert/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); diff --git a/showcase/tests/integration/components/hds/app-footer/copyright-test.js b/showcase/tests/integration/components/hds/app-footer/copyright-test.gts similarity index 65% rename from showcase/tests/integration/components/hds/app-footer/copyright-test.js rename to showcase/tests/integration/components/hds/app-footer/copyright-test.gts index ffea2d39105..6051097517a 100644 --- a/showcase/tests/integration/components/hds/app-footer/copyright-test.js +++ b/showcase/tests/integration/components/hds/app-footer/copyright-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooterCopyright } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-footer/copyright', function (hooks) { setupRenderingTest(hooks); @@ -14,20 +16,26 @@ module('Integration | Component | hds/app-footer/copyright', function (hooks) { const currentYear = new Date().getFullYear(); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-copyright').hasClass('hds-app-footer__copyright'); }); // OPTIONS test('it renders the copyright with the current year by default', async function (assert) { - await render(hbs``); - assert.dom('#test-copyright').includesText(currentYear); + await render( + , + ); + assert.dom('#test-copyright').includesText(`${currentYear}`); }); test('it renders the copyright with the passed in year value', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copyright').includesText('1984'); }); diff --git a/showcase/tests/integration/components/hds/app-footer/index-test.js b/showcase/tests/integration/components/hds/app-footer/index-test.gts similarity index 58% rename from showcase/tests/integration/components/hds/app-footer/index-test.js rename to showcase/tests/integration/components/hds/app-footer/index-test.gts index 1c3f4abc169..aaa9eb1940d 100644 --- a/showcase/tests/integration/components/hds/app-footer/index-test.js +++ b/showcase/tests/integration/components/hds/app-footer/index-test.gts @@ -4,38 +4,46 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooter } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-footer/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-footer').hasClass('hds-app-footer'); }); // CONTENT test('it renders the default content', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-app-footer__copyright').exists(); }); test('it renders the passed in content', async function (assert) { - await render(hbs` - - Before - Custom item - - Custom link - - - - After - - `); + await render( + , + ); assert.dom('#test-extra-before').hasText('Before'); assert.dom('#test-custom-item').hasText('Custom item'); assert @@ -51,12 +59,14 @@ module('Integration | Component | hds/app-footer/index', function (hooks) { // OPTIONS test('it renders with the default "light" theme', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-footer').hasClass('hds-app-footer--theme-light'); }); test('it renders with the passed in "dark" theme', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-app-footer').hasClass('hds-app-footer--theme-dark'); }); }); diff --git a/showcase/tests/integration/components/hds/app-footer/item-test.js b/showcase/tests/integration/components/hds/app-footer/item-test.gts similarity index 69% rename from showcase/tests/integration/components/hds/app-footer/item-test.js rename to showcase/tests/integration/components/hds/app-footer/item-test.gts index 97246a9f68a..588615d1fe5 100644 --- a/showcase/tests/integration/components/hds/app-footer/item-test.js +++ b/showcase/tests/integration/components/hds/app-footer/item-test.gts @@ -4,15 +4,21 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooterItem } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-footer/item', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs`
`); + await render( + , + ); assert.dom('#test-item').hasClass('hds-app-footer__list-item'); }); @@ -20,7 +26,9 @@ module('Integration | Component | hds/app-footer/item', function (hooks) { test('it renders text content yielded within the Item', async function (assert) { await render( - hbs`
    Custom item
`, + , ); assert.dom('#test-item').hasText('Custom item'); }); diff --git a/showcase/tests/integration/components/hds/app-footer/legal-links-test.js b/showcase/tests/integration/components/hds/app-footer/legal-links-test.gts similarity index 76% rename from showcase/tests/integration/components/hds/app-footer/legal-links-test.js rename to showcase/tests/integration/components/hds/app-footer/legal-links-test.gts index c10734686bb..86370690916 100644 --- a/showcase/tests/integration/components/hds/app-footer/legal-links-test.js +++ b/showcase/tests/integration/components/hds/app-footer/legal-links-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooterLegalLinks } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/app-footer/legal-links', @@ -15,7 +17,9 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs`
`, + , ); assert.dom('#test-legal-links').hasClass('hds-app-footer__legal-links'); }); @@ -24,7 +28,9 @@ module( test('it contains the default links with default href values', async function (assert) { await render( - hbs`
`, + , ); assert .dom('#test-legal-links li:nth-child(1) a') @@ -51,16 +57,18 @@ module( // OPTIONS test('it uses the passed in custom href values', async function (assert) { - await render(hbs` -
- `); + await render( + , + ); assert .dom('#test-legal-links li:nth-child(1) a') .hasText('Support') diff --git a/showcase/tests/integration/components/hds/app-footer/link-test.js b/showcase/tests/integration/components/hds/app-footer/link-test.gts similarity index 63% rename from showcase/tests/integration/components/hds/app-footer/link-test.js rename to showcase/tests/integration/components/hds/app-footer/link-test.gts index bc47c0e285b..de8307ee467 100644 --- a/showcase/tests/integration/components/hds/app-footer/link-test.js +++ b/showcase/tests/integration/components/hds/app-footer/link-test.gts @@ -4,20 +4,25 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooterLink } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-footer/link', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs` -
    - - Custom link - -
`); + await render( + , + ); assert.dom('#test-link').hasClass('hds-app-footer__link'); }); @@ -25,12 +30,13 @@ module('Integration | Component | hds/app-footer/link', function (hooks) { test('it renders text content yielded within the Link', async function (assert) { await render( - hbs` + , ); assert .dom('#test-link') diff --git a/showcase/tests/integration/components/hds/app-footer/status-link-test.js b/showcase/tests/integration/components/hds/app-footer/status-link-test.gts similarity index 68% rename from showcase/tests/integration/components/hds/app-footer/status-link-test.js rename to showcase/tests/integration/components/hds/app-footer/status-link-test.gts index f594c3245b1..ff22b2610b9 100644 --- a/showcase/tests/integration/components/hds/app-footer/status-link-test.js +++ b/showcase/tests/integration/components/hds/app-footer/status-link-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFooterStatusLink } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/app-footer/status-link', @@ -15,7 +17,12 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs`
`, + , ); assert.dom('#test-status-link').hasClass('hds-app-footer__status-link'); }); @@ -25,12 +32,22 @@ module( // status test('it should display text, icon, and icon color matching the passed in status', async function (assert) { - await render(hbs``); + await render( + , + ); // operational assert .dom('#test-operational') @@ -68,13 +85,15 @@ module( // text, statusIcon, statusIconColor test('it should display the custom text, icon color, and icon passed in', async function (assert) { - await render(hbs` -
- `); + await render( + , + ); assert.dom('.hds-app-footer__status-link').hasText('Waypoint'); assert.dom('.hds-app-footer__status-link .hds-icon').exists(); // .hasStyle({'--hds-app-footer-status-icon-color': 'var(--token-color-waypoint-brand)'}) @@ -83,9 +102,14 @@ module( // href test('it should use the passed in href for the link', async function (assert) { - await render(hbs` -
- `); + await render( + , + ); assert .dom('.hds-app-footer__status-link') .hasAttribute('href', 'https://www.hashicorp.com/custom-url'); @@ -100,7 +124,11 @@ module( setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs`
`); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); @@ -113,7 +141,14 @@ module( setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs`
`); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); diff --git a/showcase/tests/integration/components/hds/app-frame/index-test.js b/showcase/tests/integration/components/hds/app-frame/index-test.gts similarity index 75% rename from showcase/tests/integration/components/hds/app-frame/index-test.js rename to showcase/tests/integration/components/hds/app-frame/index-test.gts index 5ca9119e9ca..ccb1598e437 100644 --- a/showcase/tests/integration/components/hds/app-frame/index-test.js +++ b/showcase/tests/integration/components/hds/app-frame/index-test.gts @@ -4,27 +4,33 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppFrame } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-frame/index', function (hooks) { setupRenderingTest(hooks); test('it should render with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-frame').hasClass('hds-app-frame'); }); // CONTENT test('it should yield the different content areas (and spreads attributes on them)', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('#test-app-frame[data-test-app-frame]').exists(); @@ -79,63 +86,75 @@ module('Integration | Component | hds/app-frame/index', function (hooks) { // hasHeader test('it should hide the header when @hasHeader is false', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('#test-app-frame-header').doesNotExist(); }); // hasSidebar test('it should hide the sidebar when @hasSidebar is false', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('#test-app-frame-sidebar').doesNotExist(); }); // hasFooter test('it should hide the sidebar when @hasFooter is false', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('#test-app-frame-sidebar').doesNotExist(); }); // hasModals test('it should hide the modals when @hasModals is false', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('#test-app-frame-modals').doesNotExist(); }); // Main id test('it should have a default id of "hds-main" on the main container', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('main#hds-main').exists(); }); test('it should allow a custom id for the main container to be passed in', async function (assert) { - await render(hbs` - + await render( + , + ); assert.dom('main#test-main').exists(); }); }); diff --git a/showcase/tests/integration/components/hds/app-header/home-link-test.js b/showcase/tests/integration/components/hds/app-header/home-link-test.gts similarity index 62% rename from showcase/tests/integration/components/hds/app-header/home-link-test.js rename to showcase/tests/integration/components/hds/app-header/home-link-test.gts index e35487b1216..217dd2fa47a 100644 --- a/showcase/tests/integration/components/hds/app-header/home-link-test.js +++ b/showcase/tests/integration/components/hds/app-header/home-link-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppHeaderHomeLink } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/app-header/home-link', function (hooks) { setupRenderingTest(hooks); @@ -17,11 +19,13 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-home-link').hasClass('hds-app-header__home-link'); }); @@ -30,12 +34,14 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { test('it renders the passed in args', async function (assert) { await render( - hbs``, + , ); assert.dom('.hds-icon-hashicorp').exists(); assert @@ -46,12 +52,14 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { test('it renders the logo with a custom passed in color', async function (assert) { await render( - hbs``, + , ); assert .dom('.hds-icon-boundary') @@ -60,13 +68,15 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { test('it renders the logo with text when @isIconOnly is false', async function (assert) { await render( - hbs``, + , ); assert.dom('.hds-text').exists(); }); @@ -80,7 +90,12 @@ module('Integration | Component | hds/app-header/home-link', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); diff --git a/showcase/tests/integration/components/hds/app-header/index-test.js b/showcase/tests/integration/components/hds/app-header/index-test.gts similarity index 65% rename from showcase/tests/integration/components/hds/app-header/index-test.js rename to showcase/tests/integration/components/hds/app-header/index-test.gts index 96f1b91805c..2c0c3c35575 100644 --- a/showcase/tests/integration/components/hds/app-header/index-test.js +++ b/showcase/tests/integration/components/hds/app-header/index-test.gts @@ -4,32 +4,43 @@ */ import { module, test } from 'qunit'; +import { on } from '@ember/modifier'; +import { render, click, triggerKeyEvent, find } from '@ember/test-helpers'; + +import { + HdsAppHeader, + HdsAppHeaderHomeLink, + HdsButton, +} from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, click, triggerKeyEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/app-header/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-header').hasClass('hds-app-header'); }); // CONTENT test('it renders content passed into the globalActions and utilityActions named blocks', async function (assert) { - await render(hbs` - <:logo> - Global Item Before - - <:globalActions> - Global Item After - - <:utilityActions> - Utility Item - -`); + await render( + , + ); assert.dom('#test-global-item-before').hasText('Global Item Before'); assert.dom('#test-global-item-after').hasText('Global Item After'); assert.dom('#test-utility-item').hasText('Utility Item'); @@ -38,12 +49,12 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // RESPONSIVENESS test('it is "desktop" by default', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-header').hasClass('hds-app-header--is-desktop'); }); test('it does not show a menu button on wide viewports', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-app-header__menu-button').doesNotExist(); }); @@ -53,27 +64,34 @@ module('Integration | Component | hds/app-header/index', function (hooks) { test('it is "mobile" on narrow viewports', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-app-header').hasClass('hds-app-header--is-mobile'); }); test('it shows a menu button on narrow viewports', async function (assert) { - await render(hbs` -`); + await render(); assert.dom('.hds-app-header__menu-button').exists(); }); // Mobile menu functionality test(`the actions do not display by default on narrow viewports`, async function (assert) { - await render(hbs` -`); + await render( + , + ); assert.dom('#test-app-header').hasClass('hds-app-header--menu-is-closed'); }); test(`the actions show/hide when the menu button is pressed on narrow viewports`, async function (assert) { - await render(hbs` -`); + await render( + , + ); assert.dom('#test-app-header').hasClass('hds-app-header--menu-is-closed'); await click('.hds-app-header__menu-button'); @@ -85,23 +103,34 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // Close callback test('it should hide the actions when the "close" function is called in mobile view', async function (assert) { - await render(hbs` - - <:logo as |actions|> - - - <:globalActions as |actions|> - - - <:utilityActions as |actions|> - - -`); + await render( + , + ); // test logo actions close await click('.hds-app-header__menu-button'); @@ -123,15 +152,26 @@ module('Integration | Component | hds/app-header/index', function (hooks) { }); test('it should not do anything when the "close" function is called in desktop view', async function (assert) { - await render(hbs` - - <:globalActions as |actions|> - - - <:utilityActions as |actions|> - - -`); + await render( + , + ); assert.dom('#test-app-header').hasClass('hds-app-header--is-desktop'); assert .dom('#test-app-header') @@ -167,14 +207,14 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // Note: We pass in a high custom breakpoint to force the component to render as "mobile" test('it uses the custom passed in breakpoint to control menu display', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-app-header__menu-button').exists(); }); // A11Y test(`it displays the correct value for aria-expanded when actions are displayed vs hidden`, async function (assert) { - await render(hbs``); + await render(); await click('.hds-app-header__menu-button'); assert .dom('.hds-app-header__menu-button') @@ -188,7 +228,9 @@ module('Integration | Component | hds/app-header/index', function (hooks) { test('the actions menu collapses when the ESC key is pressed on narrow viewports', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-app-header').hasClass('hds-app-header--menu-is-closed'); @@ -200,16 +242,17 @@ module('Integration | Component | hds/app-header/index', function (hooks) { }); test('the menu button has an aria-controls attribute with a value matching the menu id', async function (assert) { - await render(hbs``); + await render(); await click('.hds-app-header__menu-button'); assert.dom('.hds-app-header__menu-button').hasAttribute('aria-controls'); assert.dom('.hds-app-header__actions').hasAttribute('id'); + const menuButton = find('.hds-app-header__menu-button'); + const actions = find('.hds-app-header__actions'); + assert.strictEqual( - this.element - .querySelector('.hds-app-header__menu-button') - .getAttribute('aria-controls'), - this.element.querySelector('.hds-app-header__actions').getAttribute('id'), + menuButton?.getAttribute('aria-controls'), + actions?.getAttribute('id'), ); // Toggle the menu back to close to avoid interfering with other tests await click('.hds-app-header__menu-button'); @@ -218,7 +261,7 @@ module('Integration | Component | hds/app-header/index', function (hooks) { // A11Y Refocus test('it renders the `a11y-refocus` elements by default with a default skip link href value of "#hds-main', async function (assert) { - await render(hbs``); + await render(); assert.dom('#ember-a11y-refocus-nav-message').exists(); assert .dom('#ember-a11y-refocus-skip-link') @@ -227,11 +270,15 @@ module('Integration | Component | hds/app-header/index', function (hooks) { }); test('it renders the `a11y-refocus` elements with the right properties provided as arguments', async function (assert) { - await render(hbs``); + await render( + , + ); assert .dom('#ember-a11y-refocus-nav-message') .hasText('test-navigation-text'); @@ -242,7 +289,9 @@ module('Integration | Component | hds/app-header/index', function (hooks) { }); test('it does not render the `a11y-refocus` elements if `hasA11yRefocus` is false', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#ember-a11y-refocus-nav-message').doesNotExist(); assert.dom('#ember-a11y-refocus-skip-link').doesNotExist(); }); diff --git a/showcase/tests/integration/components/hds/app-side-nav/index-test.js b/showcase/tests/integration/components/hds/app-side-nav/index-test.gts similarity index 53% rename from showcase/tests/integration/components/hds/app-side-nav/index-test.js rename to showcase/tests/integration/components/hds/app-side-nav/index-test.gts index 23ca9d9f657..3175e7c92f6 100644 --- a/showcase/tests/integration/components/hds/app-side-nav/index-test.js +++ b/showcase/tests/integration/components/hds/app-side-nav/index-test.gts @@ -5,77 +5,90 @@ import { module, test } from 'qunit'; import { - setupRenderingTest, - cleanupBodyOverflow, -} from 'showcase/tests/helpers'; -import { - render, click, + render, resetOnerror, settled, - triggerKeyEvent, tab, + triggerKeyEvent, } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -class MockEventTarget extends EventTarget {} - -module('Integration | Component | hds/app-side-nav/index', function (hooks) { - setupRenderingTest(hooks); +import { TrackedArray, TrackedObject } from 'tracked-built-ins'; - hooks.beforeEach(function () { - // Mock window.matchMedia to control media query events - let mockMedia = new MockEventTarget(); - mockMedia.matches = true; +import { HdsAppSideNav } from '@hashicorp/design-system-components/components'; - this.__matchMedia = window.matchMedia; +import { + cleanupBodyOverflow, + setupRenderingTest, +} from 'showcase/tests/helpers'; - this.mockMedia = () => { - window.matchMedia = () => mockMedia; - }; +class MockMediaQueryList extends EventTarget { + matches: boolean; + media: string; + onchange: ((ev: MediaQueryListEvent) => unknown) | null = null; + + constructor(matches: boolean, media: string = '') { + super(); + this.matches = matches; + this.media = media; + } + + addEventListener(type: string, listener: EventListenerOrEventListenerObject) { + super.addEventListener(type, listener); + } + + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + ) { + super.removeEventListener(type, listener); + } + + addListener(): void {} + removeListener(): void {} + + dispatchEvent(event: Event): boolean { + if (event.type === 'change' && this.onchange) { + this.onchange(event as MediaQueryListEvent); + } + return super.dispatchEvent(event); + } +} - this.changeBrowserSize = async (isDesktop) => { - mockMedia.matches = isDesktop; - mockMedia.dispatchEvent( - new MediaQueryListEvent('change', { - matches: isDesktop, - }), - ); - await settled(); - }; - }); +module('Integration | Component | hds/app-side-nav/index', function (hooks) { + setupRenderingTest(hooks); hooks.afterEach(function () { resetOnerror(); cleanupBodyOverflow(); - window.matchMedia = this.__matchMedia; }); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render( - hbs``, - ); + await render(); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav'); }); // CONTENT test('it renders content passed to the named blocks', async function (assert) { - await render(hbs` - -`); + await render( + , + ); assert.dom('#test-app-side-nav-body').exists(); }); // RESPONSIVENESS test('it is "desktop" by default', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-desktop'); }); test('it is "responsive" by default', async function (assert) { - await render(hbs``); + await render(); assert .dom('#test-app-side-nav') .hasClass('hds-app-side-nav--is-responsive'); @@ -83,7 +96,9 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { test('it is not "responsive" if `isResponsive` is false', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-app-side-nav') @@ -94,25 +109,31 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { test('it is "mobile" on narrow viewports', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-mobile'); }); test('it is minimized/collapsed on narrow viewports by default', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); }); test('it is not minimized/collapsed on narrow viewports if `isResponsive` is false', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-app-side-nav') @@ -121,28 +142,34 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { test('it shows a toggle button on narrow viewports by default', async function (assert) { await render( - hbs``, + , ); assert.dom('.hds-app-side-nav__toggle-button').exists(); }); test('it does not show a toggle button on narrow viewports if `isResponsive` is false', async function (assert) { await render( - hbs``, + , ); assert.dom('.hds-app-side-nav__toggle-button').doesNotExist(); }); test('it expands/collapses when the toggle button is pressed on narrow viewports', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); await click('.hds-app-side-nav__toggle-button'); assert @@ -154,12 +181,15 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { await click('.hds-app-side-nav__toggle-button'); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); }); test('it collapses when the ESC key is pressed on narrow viewports', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); await click('.hds-app-side-nav__toggle-button'); assert @@ -175,7 +205,9 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { test('it responds to different events to toggle between "non-minimized" (by default) and "mimimized" states', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-app-side-nav') @@ -191,9 +223,13 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { }); test('the "non-minimized" and "minimized" states have impact on its internal properties', async function (assert) { - await render(hbs` - -`); + await render( + , + ); assert .dom('#test-app-side-nav') .hasClass('hds-app-side-nav--is-not-minimized'); @@ -205,7 +241,7 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { .hasClass('hds-icon-chevrons-left'); assert.dom('.hds-app-side-nav__wrapper-body').doesNotHaveAttribute('inert'); assert.dom('#test-app-side-nav-body').doesNotHaveAttribute('inert'); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); await click('.hds-app-side-nav__toggle-button'); @@ -217,28 +253,44 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { .dom('.hds-app-side-nav__toggle-button .hds-icon') .hasClass('hds-icon-chevrons-right'); assert.dom('.hds-app-side-nav__wrapper-body').hasAttribute('inert'); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); }); test('when the viewport changes from desktop to mobile, it automatically collapses and becomes inert', async function (assert) { - this.mockMedia(); + const mockMedia = new MockMediaQueryList(true); - let calls = []; - this.setProperties({ - onDesktopViewportChange: (...args) => calls.push(args), - }); + window.matchMedia = () => mockMedia; - await render(hbs``); + const changeBrowserSize = async (isDesktop: boolean) => { + mockMedia.matches = isDesktop; + mockMedia.dispatchEvent( + new MediaQueryListEvent('change', { + matches: isDesktop, + }), + ); + await settled(); + }; + + const calls = new TrackedArray([]); + const onDesktopViewportChange = (args: boolean) => { + calls.push(args); + }; + + await render( + , + ); assert.strictEqual(calls.length, 1, 'called with initial viewport'); - await this.changeBrowserSize(false); + await changeBrowserSize(false); assert.deepEqual( calls[1], - [false], + false, 'resizing to mobile triggers a false event', ); @@ -246,55 +298,88 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { }); test('when collapsed and the viewport changes from mobile to desktop, it automatically expands and is no longer inert', async function (assert) { - this.mockMedia(); + const mockMedia = new MockMediaQueryList(true); - let calls = []; - this.setProperties({ - onDesktopViewportChange: (...args) => calls.push(args), - }); + window.matchMedia = () => mockMedia; - await render(hbs``); + const changeBrowserSize = async (isDesktop: boolean) => { + mockMedia.matches = isDesktop; + mockMedia.dispatchEvent( + new MediaQueryListEvent('change', { + matches: isDesktop, + }), + ); + await settled(); + }; + + const calls = new TrackedArray([]); + const onDesktopViewportChange = (args: boolean) => { + calls.push(args); + }; + + await render( + , + ); await click('.hds-app-side-nav__toggle-button'); assert.dom('.hds-app-side-nav__wrapper-body').hasAttribute('inert'); - await this.changeBrowserSize(false); + await changeBrowserSize(false); assert.deepEqual( calls[1], - [false], + false, 'resizing to mobile triggers a false event', ); assert.dom('.hds-app-side-nav__wrapper-body').hasAttribute('inert'); - await this.changeBrowserSize(true); + await changeBrowserSize(true); assert.deepEqual( calls[2], - [true], + true, 'resizing to desktop triggers a true event', ); assert.dom('.hds-app-side-nav__wrapper-body').doesNotHaveAttribute('inert'); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); }); test('when collapsed and the viewport changes from mobile to desktop and is expanded, scrolling is enabled', async function (assert) { - this.mockMedia(); + const mockMedia = new MockMediaQueryList(true); - let calls = []; - this.setProperties({ - onDesktopViewportChange: (...args) => calls.push(args), - }); + window.matchMedia = () => mockMedia; + + const changeBrowserSize = async (isDesktop: boolean) => { + mockMedia.matches = isDesktop; + mockMedia.dispatchEvent( + new MediaQueryListEvent('change', { + matches: isDesktop, + }), + ); + await settled(); + }; + + const calls = new TrackedArray([]); + + const onDesktopViewportChange = (args: boolean) => { + calls.push(args); + }; - await render(hbs``); - await this.changeBrowserSize(false); + await render( + , + ); + await changeBrowserSize(false); assert.deepEqual( calls[1], - [false], + false, 'resizing to mobile triggers a false event', ); @@ -304,38 +389,56 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { overflow: 'hidden', }); - await this.changeBrowserSize(true); + await changeBrowserSize(true); assert.deepEqual( calls[2], - [true], + true, 'resizing to desktop triggers a true event', ); - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); }); test('when expanded in mobile and the component is removed from the DOM, scrolling is enabled', async function (assert) { - this.mockMedia(); - - let calls = []; - this.setProperties({ - onDesktopViewportChange: (...args) => calls.push(args), + const calls = new TrackedArray([]); + const context = new TrackedObject({ + isAppSideNavRendered: false, }); - this.set('isAppSideNavRendered', true); + const onDesktopViewportChange = (args: boolean) => { + calls.push(args); + }; - await render(hbs`{{#if this.isAppSideNavRendered}} - -{{/if}}`); + const mockMedia = new MockMediaQueryList(true); - await this.changeBrowserSize(false); + window.matchMedia = () => mockMedia; + + const changeBrowserSize = async (isDesktop: boolean) => { + mockMedia.matches = isDesktop; + mockMedia.dispatchEvent( + new MediaQueryListEvent('change', { + matches: isDesktop, + }), + ); + await settled(); + }; + + await render( + , + ); + + await changeBrowserSize(false); assert.deepEqual( calls[1], - [false], + false, 'resizing to mobile triggers a false event', ); @@ -345,18 +448,19 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { overflow: 'hidden', }); - this.set('isAppSideNavRendered', false); + context.isAppSideNavRendered = false; - assert.dom('body', document).doesNotHaveStyle('overflow'); + assert.dom('body', document).doesNotHaveStyle({ overflow: 'hidden' }); }); test('when collapsed, the content in the AppSideNav is not focusable', async function (assert) { - await render(hbs` - -`); + await render( + , + ); await click('.hds-app-side-nav__toggle-button'); assert.dom('#test-app-side-nav').hasClass('hds-app-side-nav--is-minimized'); @@ -369,13 +473,23 @@ module('Integration | Component | hds/app-side-nav/index', function (hooks) { // CALLBACKS test('it should call `onToggleMinimizedStatus` function if provided', async function (assert) { - let toggled = false; - this.set('onToggleMinimizedStatus', () => (toggled = true)); - await render(hbs``); + const context = new TrackedObject({ + isToggled: false, + }); + + const onToggleMinimizedStatus = () => { + context.isToggled = true; + }; + + await render( + , + ); await click('.hds-app-side-nav__toggle-button'); - assert.ok(toggled); + assert.ok(context.isToggled); }); }); diff --git a/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js b/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.gts similarity index 66% rename from showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js rename to showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.gts index fbd1ea6f8a8..de0de63cf80 100644 --- a/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.js +++ b/showcase/tests/integration/components/hds/app-side-nav/list/back-link-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsAppSideNavListBackLink } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/app-side-nav/list/back-link', @@ -17,9 +19,12 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-app-side-nav-list-item-link-back-link') @@ -30,7 +35,9 @@ module( test('it renders the passed in args', async function (assert) { await render( - hbs``, + , ); assert.dom('.hds-icon-chevron-left').exists(); assert @@ -41,7 +48,11 @@ module( // GENERATED ELEMENTS test('it should render a - - `); + await render( + , + ); assert.dom('#test-toolbar-button').exists(); }); // @hasCopyButton test('it should render a copy button when the `@hasCopyButton` argument is true', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__copy-button') @@ -107,12 +141,20 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { .hasAria('label', 'Copy'); }); test('it should not render a copy button when the `@hasCopyButton` argument is not provided', async function (assert) { - await setupCodeEditor(hbs``); + await render( + , + ); assert.dom('.hds-code-editor__copy-button').doesNotExist(); }); test('it renders a copy button with custom text', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__copy-button') @@ -121,17 +163,26 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { }); // @isStandalone test('it should render the component with a standalone style when the `@isStandalone` argument is true and when the argument is ommitted', async function (assert) { - this.set('isStandalone', true); + const context = new TrackedObject<{ isStandalone: boolean | undefined }>({ + isStandalone: true, + }); - await setupCodeEditor( - hbs``, + await render( + , ); assert.dom('.hds-code-editor').hasClass('hds-code-editor--is-standalone'); - this.set('isStandalone', undefined); + context.isStandalone = undefined; + await settled(); assert.dom('.hds-code-editor').hasClass('hds-code-editor--is-standalone'); - this.set('isStandalone', false); + context.isStandalone = false; + await settled(); assert .dom('.hds-code-editor') .doesNotHaveClass('hds-code-editor--is-standalone'); @@ -139,46 +190,51 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @isLintingEnabled test('it should render the component with the correct aria-describedby combination when the `@isLintingEnabled` argument is true and a description is set', async function (assert) { - await setupCodeEditor( - hbs`Test Description`, + await render( + , ); - const editorContentElement = document.querySelector( - '.hds-code-editor__editor .cm-editor [role="textbox"]', - ); - const ariaDescribedBy = - editorContentElement.getAttribute('aria-describedby'); - const ariaDescribedByArray = ariaDescribedBy.split(' '); - - assert.ok(ariaDescribedByArray.includes('test-description')); - assert.ok( - ariaDescribedByArray.some((id) => - id.startsWith('lint-panel-instructions'), - ), - ); + await waitFor('.cm-editor'); + + const editor = find('.hds-code-editor__editor'); + const instructionsId = `lint-panel-instructions-${editor?.id}`; + + assert + .dom('.cm-editor [role="textbox"]') + .hasAria('describedby', `test-description ${instructionsId}`); }); // @hasFullScreenButton test('it should render a toggle fullscreen button when the `@hasFullScreenButton` argument is true', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert.dom('.hds-code-editor__full-screen-button').exists(); }); test('it should not render a toggle fullscreen button when the `@hasFullScreenButton` argument is not provided', async function (assert) { - await setupCodeEditor(hbs``); + await render( + , + ); assert.dom('.hds-code-editor__full-screen-button').doesNotExist(); }); // expand/colapse test('it should expand the code editor when the toggle full screen button is clicked', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); // initial state assert @@ -217,30 +273,32 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // copy test('it should copy the code editor value to the clipboard when the copy button is clicked', async function (assert) { const clipboardStub = sinon.stub(window.navigator.clipboard, 'writeText'); - - this.setProperties({ - handleInput: () => {}, - handleSetup: (editorView) => { - this.set('editorView', editorView); - }, + const context = new TrackedObject<{ editorView: EditorView | undefined }>({ + editorView: undefined, }); - await setupCodeEditor( - hbs``, + const handleSetup = (editorView: EditorView) => { + context.editorView = editorView; + }; + + await render( + , ); await click('.hds-code-editor__copy-button'); assert.true(clipboardStub.calledWith('Test Code')); - this.editorView.dispatch({ + context.editorView?.dispatch({ changes: { - from: this.editorView.state.selection.main.from, + from: context.editorView.state.selection.main.from, insert: 'Additional text. ', }, }); @@ -253,8 +311,13 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @ariaDescribedBy test('it should render the component with an aria-describedby when provided', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') @@ -263,8 +326,8 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @ariaLabel test('it should render the component with an aria-label when provided', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') @@ -273,16 +336,21 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @ariaLabelledBy test('it should render the component with an aria-labelledby when provided', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') .hasAttribute('aria-labelledby', 'test-label'); }); test('it should not render the component with an aria-labbelledby when @ariaLabel is provided as well', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') @@ -294,16 +362,24 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @hasLineWrapping test('it should render the editor with line wrapping enabled when hasLineWrapping is true and not when it is false', async function (assert) { - this.set('hasLineWrapping', true); + const context = new TrackedObject({ + hasLineWrapping: true, + }); - await setupCodeEditor( - hbs``, + await render( + , ); assert .dom('.hds-code-editor__editor .cm-editor .cm-content') .hasClass('cm-lineWrapping'); - this.set('hasLineWrapping', false); + context.hasLineWrapping = false; + await settled(); assert .dom('.hds-code-editor__editor .cm-editor .cm-content') .doesNotHaveClass('cm-lineWrapping'); @@ -311,8 +387,10 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @value test('it should render the component with the provided value', async function (assert) { - await setupCodeEditor( - hbs``, + await render( + , ); assert.dom('.hds-code-editor__editor .cm-editor').includesText('Test Code'); }); @@ -320,21 +398,27 @@ module('Integration | Component | hds/code-editor/index', function (hooks) { // @onInput test('it should call the onInput action when the code editor value changes', async function (assert) { const inputSpy = sinon.spy(); - - this.setProperties({ - handleInput: inputSpy, - handleSetup: (editorView) => { - this.set('editorView', editorView); - }, + const context = new TrackedObject<{ editorView: EditorView | undefined }>({ + editorView: undefined, }); - await setupCodeEditor( - hbs``, + const handleSetup = (editorView: EditorView) => { + context.editorView = editorView; + }; + + await render( + , ); - this.editorView.dispatch({ + context.editorView?.dispatch({ changes: { - from: this.editorView.state.selection.main.from, + from: context.editorView.state.selection.main.from, insert: 'Test string', }, }); diff --git a/showcase/tests/integration/components/hds/code-editor/title-test.js b/showcase/tests/integration/components/hds/code-editor/title-test.gts similarity index 65% rename from showcase/tests/integration/components/hds/code-editor/title-test.js rename to showcase/tests/integration/components/hds/code-editor/title-test.gts index 52807b48fe5..7f0c6131289 100644 --- a/showcase/tests/integration/components/hds/code-editor/title-test.js +++ b/showcase/tests/integration/components/hds/code-editor/title-test.gts @@ -4,29 +4,32 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; import sinon from 'sinon'; +import { HdsCodeEditorTitle } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; +import NOOP from 'showcase/utils/noop'; + module('Integration | Component | hds/code-editor/title', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - this.set('noop', () => {}); - await render( - hbs``, + , ); assert.dom('.hds-code-editor__title').exists(); }); test('it should render the component with a title using the default tag', async function (assert) { - this.set('noop', () => {}); - await render( - hbs`Test Title`, + , ); assert @@ -37,10 +40,11 @@ module('Integration | Component | hds/code-editor/title', function (hooks) { // @tag test('it shoud render the component title with a custom tag when provided', async function (assert) { - this.set('noop', () => {}); - await render( - hbs`Test Title`, + , ); assert.dom('.hds-code-editor__title').hasTagName('h1'); @@ -49,10 +53,11 @@ module('Integration | Component | hds/code-editor/title', function (hooks) { // @onInsert test('it should call the `@onInsert` action when the title is inserted', async function (assert) { const onInsert = sinon.spy(); - this.set('onInsert', onInsert); await render( - hbs`Test Title`, + , ); assert.true(onInsert.calledOnce); diff --git a/showcase/tests/integration/components/hds/copy/button/index-test.js b/showcase/tests/integration/components/hds/copy/button/index-test.gts similarity index 57% rename from showcase/tests/integration/components/hds/copy/button/index-test.js rename to showcase/tests/integration/components/hds/copy/button/index-test.gts index 4165155ce1a..3930fae5cce 100644 --- a/showcase/tests/integration/components/hds/copy/button/index-test.js +++ b/showcase/tests/integration/components/hds/copy/button/index-test.gts @@ -4,34 +4,37 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; +import { wait } from 'showcase/tests/helpers'; import sinon from 'sinon'; -import { wait } from 'showcase/tests/helpers'; +import { HdsCopyButton } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/copy/button/index', function (hooks) { setupRenderingTest(hooks); - // IMPORTANT: don't use an arrow function here or "this.set" will not be recognized - hooks.beforeEach(function () { + hooks.beforeEach(() => { sinon.stub(window.navigator.clipboard, 'writeText').resolves(); - this.success = undefined; - this.set('onSuccess', () => (this.success = true)); - this.set('onError', () => (this.success = false)); }); hooks.afterEach(() => { resetOnerror(); // we need to restore the "window.navigator" methods sinon.restore(); - this.success = undefined; }); test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button'); }); @@ -39,27 +42,70 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { // @TEXT ARGUMENT test('it should allow to copy a `string` provided as `@text` argument', async function (assert) { + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + await render( - hbs``, + , ); await click('button#test-copy-button'); - assert.true(this.success); + assert.true(context.success); }); // @TARGET ARGUMENT test('it should allow to target an element using a `string` selector for the `@target` argument', async function (assert) { + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + await render( - hbs`

Hello world!

`, + , ); await click('button#test-copy-button'); - assert.true(this.success); + assert.true(context.success); }); // @ariaMessageText ARGUMENT test('it should set a custom success message in the aria-live region if passed', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button--status-idle'); // Test the copy success message is not rendered before the button is clicked: @@ -78,7 +124,13 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { test('it should render the correct default component variation: secondary color, medium size, idle status', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button'); assert.dom('#test-copy-button').hasClass('hds-button--size-medium'); @@ -88,7 +140,14 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { test('it should only render an icon and also render an aria-label if isIconOnly is set to true', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').doesNotIncludeText('Copy'); assert.dom('#test-copy-button').hasAria('label', 'Copy'); @@ -96,15 +155,27 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { test('it should render the small size if @size small is defined', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-button--size-small'); }); test('it always renders the text value, not the text to copy', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasText('Copy your secret key'); assert @@ -114,7 +185,14 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { test('it should have the correct CSS class to support full-width size if @isFullWidth prop is true', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-button--width-full'); }); @@ -122,8 +200,26 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { // COPY STATES test('it should update the status to success if the copy operation was successful', async function (assert) { + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button--status-idle'); // Test the copy success message is not rendered before the button is clicked: @@ -132,7 +228,7 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { .doesNotContainText('Copied to clipboard'); await click('button#test-copy-button'); - assert.true(this.success); + assert.true(context.success); // Test the copy success message is rendered after the button is clicked: assert.dom('#test-copy-button').hasClass('hds-copy-button--status-success'); assert.dom('#test-copy-button + .sr-only').hasText('Copied to clipboard'); @@ -140,8 +236,13 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { test('it should update the status back to idle after success', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button--status-idle'); await click('button#test-copy-button'); @@ -154,16 +255,32 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { sinon.restore(); sinon .stub(window.navigator.clipboard, 'writeText') - .throws( - 'Sinon throws (syntethic error)', - 'this is a fake error message provided to the sinon.stub().throws() method', - ); + .throws('Sinon throws (syntethic error)'); + + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + await render( - hbs``, + , ); assert.dom('#test-copy-button').hasClass('hds-copy-button--status-idle'); await click('button#test-copy-button'); - assert.false(this.success); + assert.false(context.success); assert.dom('#test-copy-button').hasClass('hds-copy-button--status-error'); await wait(2000); // wait for the status to revert to "idle" automatically assert.dom('#test-copy-button').hasClass('hds-copy-button--status-idle'); @@ -177,8 +294,15 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); @@ -191,8 +315,17 @@ module('Integration | Component | hds/copy/button/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); diff --git a/showcase/tests/integration/components/hds/copy/snippet/index-test.js b/showcase/tests/integration/components/hds/copy/snippet/index-test.gts similarity index 61% rename from showcase/tests/integration/components/hds/copy/snippet/index-test.js rename to showcase/tests/integration/components/hds/copy/snippet/index-test.gts index 99299c919bf..9e223dd8416 100644 --- a/showcase/tests/integration/components/hds/copy/snippet/index-test.js +++ b/showcase/tests/integration/components/hds/copy/snippet/index-test.gts @@ -4,22 +4,20 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; +import { wait } from 'showcase/tests/helpers'; import sinon from 'sinon'; -import { wait } from 'showcase/tests/helpers'; +import { HdsCopySnippet } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/copy/snippet/index', function (hooks) { setupRenderingTest(hooks); - // IMPORTANT: don't use an arrow function here or "this.set" will not be recognized - hooks.beforeEach(function () { + hooks.beforeEach(() => { sinon.stub(window.navigator.clipboard, 'writeText').resolves(); - this.success = undefined; - this.set('onSuccess', () => (this.success = true)); - this.set('onError', () => (this.success = false)); }); hooks.afterEach(() => { @@ -30,14 +28,21 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet'); }); test('it should render the component with an aria-label that includes the correct copy text', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasAria('label', 'copy this aria label'); }); @@ -46,7 +51,12 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { test('it should render the correct default component variation: primary color, idle status', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet'); assert @@ -57,7 +67,13 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { test('it should render the secondary color if defined', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-copy-snippet') @@ -66,14 +82,26 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { test('it should support truncation if @isTruncated is set to true', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--is-truncated'); }); test('it should have the correct CSS class to support full-width size if @isFullWidth prop is true', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--width-full'); }); @@ -81,12 +109,29 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { // COPY STATES test('it should update the status to success if the copy operation was successful', async function (assert) { + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--status-idle'); await click('button#test-copy-snippet'); - assert.true(this.success); + assert.true(context.success); assert .dom('#test-copy-snippet') .hasClass('hds-copy-snippet--status-success'); @@ -94,7 +139,12 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { test('it should update the status back to idle after success', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--status-idle'); await click('button#test-copy-snippet'); @@ -106,19 +156,33 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { }); test('it should update the status to an error after a failed "copy" operation', async function (assert) { + const context = new TrackedObject>({ + success: undefined, + }); + const onSuccess = () => { + context.success = true; + }; + const onError = () => { + context.success = false; + }; + sinon.restore(); sinon .stub(window.navigator.clipboard, 'writeText') - .throws( - 'Sinon throws (syntethic error)', - 'this is a fake error message provided to the sinon.stub().throws() method', - ); + .throws('Sinon throws (syntethic error)'); await render( - hbs``, + , ); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--status-idle'); await click('button#test-copy-snippet'); - assert.false(this.success); + assert.false(context.success); assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--status-error'); await wait(2000); // wait for the status to revert to "idle" automatically assert.dom('#test-copy-snippet').hasClass('hds-copy-snippet--status-idle'); @@ -134,7 +198,14 @@ module('Integration | Component | hds/copy/snippet/index', function (hooks) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); await render( - hbs``, + , ); assert.throws(function () { throw new Error(errorMessage); diff --git a/showcase/tests/integration/components/hds/dialog-primitive/body-test.js b/showcase/tests/integration/components/hds/dialog-primitive/body-test.gts similarity index 70% rename from showcase/tests/integration/components/hds/dialog-primitive/body-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/body-test.gts index b4aa56f1b9a..958a6808f5c 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/body-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/body-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsDialogPrimitiveBody } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/dialog-primitive/body', function (hooks) { setupRenderingTest(hooks); @@ -17,11 +19,11 @@ module('Integration | Component | hds/dialog-primitive/body', function (hooks) { test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs` - + , ); assert.dom('#test-body').hasClass('hds-dialog-primitive__body'); }); @@ -30,11 +32,11 @@ module('Integration | Component | hds/dialog-primitive/body', function (hooks) { test('it renders the passed in content', async function (assert) { await render( - hbs` - - Body - - `, + , ); assert.dom('.hds-dialog-primitive__body').hasText('Body'); }); diff --git a/showcase/tests/integration/components/hds/dialog-primitive/description-test.js b/showcase/tests/integration/components/hds/dialog-primitive/description-test.gts similarity index 68% rename from showcase/tests/integration/components/hds/dialog-primitive/description-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/description-test.gts index 0ff8c6454b7..33af078ecbc 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/description-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/description-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsDialogPrimitiveDescription } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/dialog-primitive/description', @@ -19,11 +21,11 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs` - - Description - - `, + , ); assert .dom('#test-description') @@ -34,11 +36,11 @@ module( test('it renders the passed in content', async function (assert) { await render( - hbs` - - Description - - `, + , ); assert.dom('.hds-dialog-primitive__description').hasText('Description'); }); diff --git a/showcase/tests/integration/components/hds/dialog-primitive/footer-test.js b/showcase/tests/integration/components/hds/dialog-primitive/footer-test.gts similarity index 54% rename from showcase/tests/integration/components/hds/dialog-primitive/footer-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/footer-test.gts index 41d8c02f71f..dfd7471d9ba 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/footer-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/footer-test.gts @@ -4,9 +4,16 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { on } from '@ember/modifier'; +import { TrackedObject } from 'tracked-built-ins'; + +import { + HdsDialogPrimitiveFooter, + HdsButton, +} from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/dialog-primitive/footer', @@ -19,11 +26,11 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs` - - Footer - - `, + , ); assert.dom('#test-footer').hasClass('hds-dialog-primitive__footer'); }); @@ -32,11 +39,11 @@ module( test('it renders the passed in content', async function (assert) { await render( - hbs` - - - - `, + , ); assert.dom('.hds-dialog-primitive__footer .hds-button').exists(); }); @@ -44,17 +51,24 @@ module( // CALLBACK test('it should forwards the `onDismiss` callback function so it can be invoked as yielded function', async function (assert) { - let dismissed = false; - this.set('onDismiss', () => (dismissed = true)); + const context = new TrackedObject({ + isDismissed: false, + }); + + const onDismiss = () => { + context.isDismissed = true; + }; + await render( - hbs` - - - - `, + , ); + await click('.hds-dialog-primitive__footer .hds-button'); - assert.ok(dismissed); + assert.ok(context.isDismissed); }); }, ); diff --git a/showcase/tests/integration/components/hds/dialog-primitive/header-test.js b/showcase/tests/integration/components/hds/dialog-primitive/header-test.gts similarity index 62% rename from showcase/tests/integration/components/hds/dialog-primitive/header-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/header-test.gts index 5827148ee69..7b16765513a 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/header-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/header-test.gts @@ -4,9 +4,12 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsDialogPrimitiveHeader } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/dialog-primitive/header', @@ -19,9 +22,11 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs` - Title - `, + , ); assert.dom('#test-header').hasClass('hds-dialog-primitive__header'); }); @@ -30,11 +35,11 @@ module( test('it renders the title without icon, tagline, or description', async function (assert) { await render( - hbs` - - Title - - `, + , ); assert.dom('.hds-dialog-primitive__title').exists(); assert.dom('.hds-dialog-primitive__title').hasText('Title'); @@ -44,11 +49,11 @@ module( test('it renders the title with icon and tagline if provided', async function (assert) { await render( - hbs` - - Title - - `, + , ); assert.dom('.hds-dialog-primitive__title').exists(); assert.dom('.hds-dialog-primitive__title').hasText('Tagline Title'); @@ -58,20 +63,28 @@ module( }); test('it renders the title as a div when the @titleTag argument is not provided', async function (assert) { - await render(hbs` - - Title - - `); + await render( + , + ); assert.dom('.hds-dialog-primitive__title').hasTagName('div'); }); test('it renders the title as a custom title tag when the @titleTag argument is provided', async function (assert) { - await render(hbs` - - Title - - `); + await render( + , + ); assert.dom('.hds-dialog-primitive__title').hasTagName('h1'); }); @@ -79,11 +92,15 @@ module( test('it adds contextual classes to different DOM nodes using the `@contextualClassPrefix`', async function (assert) { await render( - hbs` - - Title - - `, + , ); assert.dom('.hds-dialog-primitive__header.abc__header').exists(); assert.dom('.hds-dialog-primitive__icon.abc__icon').exists(); @@ -96,11 +113,11 @@ module( test('it should always render the "dismiss" button', async function (assert) { await render( - hbs` - - Title - - `, + , ); assert.dom('button.hds-dialog-primitive__dismiss').exists(); }); @@ -108,17 +125,24 @@ module( // CALLBACK test('the "dismiss" button should invoke the `onDismiss` callback function', async function (assert) { - let dismissed = false; - this.set('onDismiss', () => (dismissed = true)); + const context = new TrackedObject({ + isDismissed: false, + }); + + const onDismiss = () => { + context.isDismissed = true; + }; + await render( - hbs` - - Title - - `, + , ); + await click('button.hds-dialog-primitive__dismiss'); - assert.ok(dismissed); + assert.ok(context.isDismissed); }); }, ); diff --git a/showcase/tests/integration/components/hds/dialog-primitive/overlay-test.js b/showcase/tests/integration/components/hds/dialog-primitive/overlay-test.gts similarity index 78% rename from showcase/tests/integration/components/hds/dialog-primitive/overlay-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/overlay-test.gts index 3c26e4eb8a0..47845cf06de 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/overlay-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/overlay-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsDialogPrimitiveOverlay } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/dialog-primitive/overlay', @@ -18,7 +20,7 @@ module( }); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-dialog-primitive__overlay').exists(); }); }, diff --git a/showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.js b/showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.gts similarity index 57% rename from showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.js rename to showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.gts index 13f8ce70fab..2fcdd1aaf63 100644 --- a/showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.js +++ b/showcase/tests/integration/components/hds/dialog-primitive/wrapper-test.gts @@ -4,9 +4,17 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { + HdsDialogPrimitiveBody, + HdsDialogPrimitiveDescription, + HdsDialogPrimitiveFooter, + HdsDialogPrimitiveHeader, + HdsDialogPrimitiveWrapper, +} from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/dialog-primitive/wrapper', @@ -19,13 +27,13 @@ module( test('it should render the component with a CSS class that matches the component name, and its sub', async function (assert) { await render( - hbs` - - <:header>Header - <:body>Body - <:footer>Footer - - `, + , ); assert .dom('#test-dialog-primitive') @@ -36,20 +44,21 @@ module( test('it renders the content slots and the contextual components', async function (assert) { await render( - hbs` - - <:header> - Title - Description - - <:body> - Body - - <:footer> - Footer - - - `, + , ); assert.dom('.hds-dialog-primitive__wrapper-header').exists(); assert.dom('.hds-dialog-primitive__wrapper-body').exists(); diff --git a/showcase/tests/integration/components/hds/disclosure-primitive/index-test.gts b/showcase/tests/integration/components/hds/disclosure-primitive/index-test.gts new file mode 100644 index 00000000000..54e57e59ede --- /dev/null +++ b/showcase/tests/integration/components/hds/disclosure-primitive/index-test.gts @@ -0,0 +1,313 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { click, render, resetOnerror, settled } from '@ember/test-helpers'; +import { on } from '@ember/modifier'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsDisclosurePrimitive } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +module( + 'Integration | Component | hds/disclosure-primitive/index', + function (hooks) { + setupRenderingTest(hooks); + + hooks.afterEach(() => { + resetOnerror(); + }); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + await render( + , + ); + assert + .dom('div#test-disclosure-primitive') + .hasClass('hds-disclosure-primitive'); + }); + + // TOGGLE + CONTENT + + test('it should render the "toggle" block but not the "content', async function (assert) { + await render( + , + ); + assert.dom('.hds-disclosure-primitive__toggle').exists(); + assert.dom('button#test-disclosure-primitive-button').exists(); + assert.dom('.hds-disclosure-primitive__content').exists(); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + }); + test('it should render the "content" when the "toggle" is clicked', async function (assert) { + await render( + , + ); + await click('button#test-disclosure-primitive-button'); + assert.dom('a#test-disclosure-primitive-link').exists(); + }); + + // isOpen + + test('it should toggle the "content" when @isOpen is set', async function (assert) { + const context = new TrackedObject({ + isOpen: true, + }); + + await render( + , + ); + assert.dom('a#test-disclosure-primitive-link').exists(); + + context.isOpen = false; + await settled(); + + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + }); + + test('it should allow @isOpen to override an internal _isOpen=true', async function (assert) { + const context = new TrackedObject>({ + isOpen: undefined, + }); + + await render( + , + ); + await click('button#test-toggle-button'); + assert.dom('a#test-disclosure-primitive-link').exists(); + + context.isOpen = false; + await settled(); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + }); + + test('it should allow @isOpen to override an internal _isOpen=false', async function (assert) { + const context = new TrackedObject>({ + isOpen: undefined, + }); + + await render( + , + ); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + + context.isOpen = true; + await settled(); + + assert.dom('a#test-disclosure-primitive-link').exists(); + }); + + test('it should allow the internal _isOpen to override @isOpen=true', async function (assert) { + await render( + , + ); + assert.dom('a#test-disclosure-primitive-link').exists(); + + await click('button#test-toggle-button'); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + }); + + test('it should allow the internal _isOpen to override @isOpen=false', async function (assert) { + await render( + , + ); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + + await click('button#test-toggle-button'); + assert.dom('a#test-disclosure-primitive-link').exists(); + }); + + // contentId + + test('it should set the contentId on the content block', async function (assert) { + await render( + , + ); + assert.dom('.hds-disclosure-primitive__content').hasAttribute('id'); + }); + + // CLOSE DISCLOSED CONTENT ON CLICK + + test('it should hide the "content" when an interactive element triggers `close`', async function (assert) { + await render( + , + ); + await click('button#test-toggle-button'); + assert.dom('button#test-content-button').exists(); + await click('button#test-content-button'); + assert.dom('button#test-content-button').doesNotExist(); + }); + + // CALLBACK + + test('it should invoke the `onClickToggle` callback', async function (assert) { + const context = new TrackedObject({ + isOpen: false, + }); + + const onClickToggle = () => { + context.isOpen = !context.isOpen; + }; + + await render( + , + ); + // toggle to open + await click('button#test-toggle-button'); + assert.true(context.isOpen); + assert.dom('a#test-disclosure-primitive-link').exists(); + // toggle to close + await click('button#test-toggle-button'); + assert.false(context.isOpen); + assert.dom('a#test-disclosure-primitive-link').doesNotExist(); + }); + }, +); diff --git a/showcase/tests/integration/components/hds/disclosure-primitive/index-test.js b/showcase/tests/integration/components/hds/disclosure-primitive/index-test.js deleted file mode 100644 index c007f8961cf..00000000000 --- a/showcase/tests/integration/components/hds/disclosure-primitive/index-test.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; -import { click, render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module( - 'Integration | Component | hds/disclosure-primitive/index', - function (hooks) { - setupRenderingTest(hooks); - - hooks.afterEach(() => { - resetOnerror(); - }); - - test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render( - hbs``, - ); - assert - .dom('div#test-disclosure-primitive') - .hasClass('hds-disclosure-primitive'); - }); - - // TOGGLE + CONTENT - - test('it should render the "toggle" block but not the "content', async function (assert) { - await render(hbs` - - <:toggle> - - - - `); - await click('button#test-toggle-button'); - assert.dom('button#test-content-button').exists(); - await click('button#test-content-button'); - assert.dom('button#test-content-button').doesNotExist(); - }); - - // CALLBACK - - test('it should invoke the `onClickToggle` callback', async function (assert) { - let opened = false; - this.set('onClickToggle', () => (opened = !opened)); - await render(hbs` - - <:toggle as |t|> - - {{#if this.showFlyout}} - - Title - - {{/if}} - `, + , ); await click('#test-button'); - assert.true(this.showFlyout); + assert.true(context.isFlyoutRendered); await click('button.hds-flyout__dismiss'); assert.dom('#test-button').isFocused(); }); // this test is flaky in CI, so skipping for now skip('it returns focus to the `body` element, if the one that initiated the open event not anymore in the DOM', async function (assert) { + const context = new TrackedObject({ + isFlyoutRendered: false, + }); + + const showFlyout = () => { + context.isFlyoutRendered = true; + }; + await render( - hbs` - - open flyout - - {{#if this.showFlyout}} - - Title - - {{/if}} - `, + , ); await click('#test-toggle'); await click('#test-interactive'); - assert.true(this.showFlyout); + assert.true(context.isFlyoutRendered); await click('button.hds-flyout__dismiss'); - assert.dom('body', 'document').isFocused(); + assert.dom('body', document).isFocused(); }); test('it returns focus to a specific element if provided via`@returnFocusTo`', async function (assert) { + const context = new TrackedObject({ + isFlyoutRendered: false, + }); + + const showFlyout = () => { + context.isFlyoutRendered = true; + }; + await render( - hbs` - - open flyout - - {{#if this.showFlyout}} - - Title - - {{/if}} - `, + , ); await click('#test-toggle'); await click('#test-interactive'); - assert.true(this.showFlyout); + assert.true(context.isFlyoutRendered); await click('button.hds-flyout__dismiss'); assert.dom('#test-toggle').isFocused(); }); @@ -381,30 +500,45 @@ module('Integration | Component | hds/flyout/index', function (hooks) { // CALLBACKS test('it should call `onOpen` function if provided', async function (assert) { - let opened = false; - this.set('onOpen', () => (opened = true)); + const context = new TrackedObject({ + isFlyoutOpen: false, + }); + + const onOpen = () => { + context.isFlyoutOpen = true; + }; + await render( - hbs` - Title - `, + , ); assert.dom('#test-onopen-callback').isVisible(); await settled(); - assert.ok(opened); + assert.ok(context.isFlyoutOpen); }); test('it should call `onClose` function if provided', async function (assert) { - let closed = false; - this.set('onClose', () => (closed = true)); + const context = new TrackedObject({ + isFlyoutOpen: true, + }); + + const onClose = () => { + context.isFlyoutOpen = false; + }; + await render( - hbs` - Title - `, + , ); await click('button.hds-flyout__dismiss'); assert.dom('#test-onclose-callback').isNotVisible(); - await settled(); - assert.ok(closed); + assert.ok(!context.isFlyoutOpen); }); // ASSERTIONS @@ -417,7 +551,10 @@ module('Integration | Component | hds/flyout/index', function (hooks) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); await render( - hbs`Title`, + , ); assert.throws(function () { throw new Error(errorMessage); diff --git a/showcase/tests/integration/components/hds/form/character-count/index-test.js b/showcase/tests/integration/components/hds/form/character-count/index-test.gts similarity index 54% rename from showcase/tests/integration/components/hds/form/character-count/index-test.js rename to showcase/tests/integration/components/hds/form/character-count/index-test.gts index 423b0d0acda..b65bae58a2e 100644 --- a/showcase/tests/integration/components/hds/form/character-count/index-test.js +++ b/showcase/tests/integration/components/hds/form/character-count/index-test.gts @@ -4,22 +4,24 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; +import { on } from '@ember/modifier'; import { render, typeIn } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormCharacterCount } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/character-count/index', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set('value', ''); - this.update = (event) => this.set('value', event.target.value); - }); test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-character-count') @@ -27,7 +29,12 @@ module( }); test('it should render with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-character-count').hasClass('my-class'); }); @@ -36,7 +43,9 @@ module( test('it renders a character count with the correct "id" attribute if the @controlId argument is provided', async function (assert) { await render( - hbs``, + , ); assert.dom('#character-count-my-control-id').exists(); }); @@ -44,10 +53,23 @@ module( // CONTENT test('it renders a character count with the default predefined format', async function (assert) { + const context = new TrackedObject({ + value: '', + }); + + const onInput = (event: Event) => { + context.value = (event.target as HTMLInputElement).value; + }; + await render( - hbs` - - `, + , ); assert.dom('#test-form-character-count').hasText('0 characters entered'); @@ -55,10 +77,28 @@ module( assert.dom('#test-form-character-count').hasText('2 characters entered'); }); test('it renders a character count in the predefined format when only @maxLength is set', async function (assert) { + const context = new TrackedObject({ + value: '', + }); + + const onInput = (event: Event) => { + context.value = (event.target as HTMLInputElement).value; + }; + await render( - hbs` - - `, + , ); assert.dom('#test-form-character-count').hasText('25 characters allowed'); @@ -86,10 +126,28 @@ module( .hasText('Exceeded by 4 characters'); }); test('it renders a character count in the predefined format when only @minLength is set', async function (assert) { + const context = new TrackedObject({ + value: '', + }); + + const onInput = (event: Event) => { + context.value = (event.target as HTMLInputElement).value; + }; + await render( - hbs` - - `, + , ); assert.dom('#test-form-character-count').hasText('3 characters required'); @@ -107,10 +165,29 @@ module( assert.dom('#test-form-character-count').hasText('3 characters entered'); }); test('it renders a character count in the predefined format when both @minLength and @maxLength are set', async function (assert) { + const context = new TrackedObject({ + value: '', + }); + + const onInput = (event: Event) => { + context.value = (event.target as HTMLInputElement).value; + }; + await render( - hbs` - - `, + , ); assert.dom('#test-form-character-count').hasText('3 characters required'); @@ -130,17 +207,29 @@ module( .hasText('Exceeded by 4 characters'); }); test('it renders a character count in custom format', async function (assert) { - this.set('value', 'with custom content'); await render( - hbs` - - - maxLength {{CC.maxLength}} - minLength {{CC.minLength}} - remaining {{CC.remaining}} - shortfall {{CC.shortfall}} - currentLength {{CC.currentLength}} - `, + , ); assert .dom('#test-form-character-count') @@ -152,11 +241,15 @@ module( // A11y test('it should present the character count as a live region', async function (assert) { - this.set('value', 'with default content'); await render( - hbs` - - `, + , ); assert .dom('#test-form-character-count') diff --git a/showcase/tests/integration/components/hds/form/checkbox/base-test.js b/showcase/tests/integration/components/hds/form/checkbox/base-test.gts similarity index 72% rename from showcase/tests/integration/components/hds/form/checkbox/base-test.js rename to showcase/tests/integration/components/hds/form/checkbox/base-test.gts index 49a818cdbc3..33c09b38145 100644 --- a/showcase/tests/integration/components/hds/form/checkbox/base-test.js +++ b/showcase/tests/integration/components/hds/form/checkbox/base-test.gts @@ -4,20 +4,26 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormCheckboxBase } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/checkbox/base', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-checkbox').hasClass('hds-form-checkbox'); }); test('it should convert the `indeterminate` attribute into a property', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-checkbox').doesNotHaveAttribute('indeterminate'); assert.dom('#test-form-checkbox').hasProperty('indeterminate', true); diff --git a/showcase/tests/integration/components/hds/form/checkbox/field-test.js b/showcase/tests/integration/components/hds/form/checkbox/field-test.gts similarity index 70% rename from showcase/tests/integration/components/hds/form/checkbox/field-test.js rename to showcase/tests/integration/components/hds/form/checkbox/field-test.gts index 2cb9bafb057..5e11df72377 100644 --- a/showcase/tests/integration/components/hds/form/checkbox/field-test.js +++ b/showcase/tests/integration/components/hds/form/checkbox/field-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; +import { render, resetOnerror, find } from '@ember/test-helpers'; + +import { HdsFormCheckboxField } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, resetOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/form/checkbox/field', function (hooks) { setupRenderingTest(hooks); @@ -16,21 +18,21 @@ module('Integration | Component | hds/form/checkbox/field', function (hooks) { }); test('it should render the component with the appropriate CSS class', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-form-field__control').exists(); }); // VALUE test('it should render the input with the value provided via @value argument', async function (assert) { - await render(hbs``); + await render(); assert.dom('input').hasValue('abc123'); }); // ID test('it should render the input with a custom @id', async function (assert) { - await render(hbs``); + await render(); assert.dom('input').hasAttribute('id', 'my-input'); }); @@ -38,11 +40,13 @@ module('Integration | Component | hds/form/checkbox/field', function (hooks) { test('it renders the yielded contextual components', async function (assert) { await render( - hbs` + , ); assert.dom('.hds-form-field__label').exists(); assert.dom('.hds-form-field__helper-text').exists(); @@ -51,34 +55,36 @@ module('Integration | Component | hds/form/checkbox/field', function (hooks) { assert.dom('.hds-form-field__error').exists(); }); test('it does not render the yielded contextual components if not provided', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-form-field__label').doesNotExist(); assert.dom('.hds-form-field__helper-text').doesNotExist(); assert.dom('.hds-form-field__error').doesNotExist(); }); test('it automatically provides all the ID relations between the elements', async function (assert) { await render( - hbs` + , ); // the control ID is dynamically generated - let control = this.element.querySelector('.hds-form-field__control input'); - let controlId = control.id; - assert.dom('.hds-form-field__label').hasAttribute('for', controlId); + const control = find('.hds-form-field__control input'); + + assert.dom('.hds-form-field__label').hasAttribute('for', control?.id || ''); assert .dom('.hds-form-field__helper-text') - .hasAttribute('id', `helper-text-${controlId}`); + .hasAttribute('id', `helper-text-${control?.id || ''}`); assert .dom('.hds-form-field__control input') .hasAttribute( 'aria-describedby', - `helper-text-${controlId} error-${controlId} extra`, + `helper-text-${control?.id || ''} error-${control?.id || ''} extra`, ); assert .dom('.hds-form-field__error') - .hasAttribute('id', `error-${controlId}`); + .hasAttribute('id', `error-${control?.id || ''}`); }); }); diff --git a/showcase/tests/integration/components/hds/form/checkbox/group-test.gts b/showcase/tests/integration/components/hds/form/checkbox/group-test.gts new file mode 100644 index 00000000000..4fd343ac036 --- /dev/null +++ b/showcase/tests/integration/components/hds/form/checkbox/group-test.gts @@ -0,0 +1,199 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { render, resetOnerror, settled, find } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormCheckboxGroup } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +module('Integration | Component | hds/form/checkbox/group', function (hooks) { + setupRenderingTest(hooks); + + hooks.afterEach(() => { + resetOnerror(); + }); + + test('it should render the component with an appropriate CSS class', async function (assert) { + await render( + , + ); + assert.dom('#test-form-checkbox').hasClass('hds-form-group'); + }); + + // YIELDED (CONTEXTUAL) COMPONENTS + + test('it renders the yielded contextual components and subcomponents', async function (assert) { + await render( + , + ); + assert.dom('.hds-form-group__legend').exists(); + assert.dom('.hds-form-group__legend').hasText('This is the legend'); + assert.dom('.hds-form-group__helper-text').exists(); + assert + .dom('.hds-form-group__helper-text') + .hasText('This is the group helper text'); + assert + .dom('.hds-form-group__control-fields-wrapper .hds-form-field__label') + .exists(); + assert + .dom( + '.hds-form-group__control-fields-wrapper .hds-form-field__helper-text', + ) + .exists(); + assert + .dom('.hds-form-group__control-fields-wrapper .hds-form-field__control') + .exists(); + assert.dom('.hds-form-group__control-fields-wrapper input').isChecked(); + assert + .dom('.hds-form-group__control-fields-wrapper input') + .hasValue('abc123'); + assert + .dom('.hds-form-group__control-fields-wrapper .hds-form-field__error') + .exists(); + assert.dom('.hds-form-group__error').exists(); + assert.dom('.hds-form-group__error').hasText('This is the group error'); + }); + test('it does not render the yielded contextual components if not provided', async function (assert) { + await render(); + assert.dom('.hds-form-group__legend').doesNotExist(); + assert.dom('.hds-form-group__helper-text').doesNotExist(); + assert.dom('.hds-form-group__error').doesNotExist(); + }); + test('it automatically provides all the ID relations between the elements', async function (assert) { + await render( + , + ); + // the IDs are dynamically generated + const groupHelperText = find('.hds-form-group__helper-text'); + const groupError = find('.hds-form-group__error'); + const fieldHelperText = find('.hds-form-field__helper-text'); + const fieldError = find('.hds-form-field__error'); + + assert + .dom('input') + .hasAttribute( + 'aria-describedby', + `${fieldHelperText?.id} ${fieldError?.id} ${groupHelperText?.id} ${groupError?.id}`, + ); + }); + + test('it automatically provides all the ID relations between the elements even when Error is conditionally rendered', async function (assert) { + const context = new TrackedObject({ showErrors: false }); + + await render( + , + ); + + context.showErrors = true; + await settled(); + + // the IDs are dynamically generated + const groupHelperText = find('.hds-form-group__helper-text'); + const groupError = find('.hds-form-group__error'); + const fieldHelperText = find('.hds-form-field__helper-text'); + const fieldError = find('.hds-form-field__error'); + + assert + .dom('input') + .hasAttribute( + 'aria-describedby', + `${fieldHelperText?.id} ${fieldError?.id} ${groupHelperText?.id} ${groupError?.id}`, + ); + }); + + // NAME + + test('it renders the defined name on all controls within a group', async function (assert) { + await render( + , + ); + assert + .dom('[data-test="first-control"]') + .hasAttribute('name', 'datacenter-demo'); + assert + .dom('[data-test="second-control"]') + .hasAttribute('name', 'datacenter-demo'); + }); + + // REQUIRED AND OPTIONAL + + test('it should append an indicator to the legend text and set the required attribute when user input is required', async function (assert) { + await render( + , + ); + assert.dom('legend .hds-form-indicator').exists(); + assert.dom('legend .hds-form-indicator').hasText('Required'); + assert.dom('input').hasAttribute('required'); + }); + test('it should append an indicator to the legend text when user input is optional', async function (assert) { + await render( + , + ); + assert.dom('legend .hds-form-indicator').exists(); + assert.dom('legend .hds-form-indicator').hasText('(Optional)'); + }); +}); diff --git a/showcase/tests/integration/components/hds/form/checkbox/group-test.js b/showcase/tests/integration/components/hds/form/checkbox/group-test.js deleted file mode 100644 index 71ccf74013c..00000000000 --- a/showcase/tests/integration/components/hds/form/checkbox/group-test.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, resetOnerror, settled } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module('Integration | Component | hds/form/checkbox/group', function (hooks) { - setupRenderingTest(hooks); - - hooks.afterEach(() => { - resetOnerror(); - }); - - test('it should render the component with an appropriate CSS class', async function (assert) { - await render(hbs``); - assert.dom('#test-form-checkbox').hasClass('hds-form-group'); - }); - - // YIELDED (CONTEXTUAL) COMPONENTS - - test('it renders the yielded contextual components and subcomponents', async function (assert) { - await render( - hbs` - This is the legend - This is the group helper text - - This is the control label - This is the control helper text - This is the control error - - This is the group error - `, - ); - assert.dom('.hds-form-group__legend').exists(); - assert.dom('.hds-form-group__legend').hasText('This is the legend'); - assert.dom('.hds-form-group__helper-text').exists(); - assert - .dom('.hds-form-group__helper-text') - .hasText('This is the group helper text'); - assert - .dom('.hds-form-group__control-fields-wrapper .hds-form-field__label') - .exists(); - assert - .dom( - '.hds-form-group__control-fields-wrapper .hds-form-field__helper-text', - ) - .exists(); - assert - .dom('.hds-form-group__control-fields-wrapper .hds-form-field__control') - .exists(); - assert.dom('.hds-form-group__control-fields-wrapper input').isChecked(); - assert - .dom('.hds-form-group__control-fields-wrapper input') - .hasValue('abc123'); - assert - .dom('.hds-form-group__control-fields-wrapper .hds-form-field__error') - .exists(); - assert.dom('.hds-form-group__error').exists(); - assert.dom('.hds-form-group__error').hasText('This is the group error'); - }); - test('it does not render the yielded contextual components if not provided', async function (assert) { - await render(hbs``); - assert.dom('.hds-form-group__legend').doesNotExist(); - assert.dom('.hds-form-group__helper-text').doesNotExist(); - assert.dom('.hds-form-group__error').doesNotExist(); - }); - test('it automatically provides all the ID relations between the elements', async function (assert) { - await render( - hbs` - This is the legend - This is the group helper text - - This is the control label - This is the control helper text - This is the control error - - This is the group error - `, - ); - // the IDs are dynamically generated - let groupHelperText = this.element.querySelector( - '.hds-form-group__helper-text', - ); - let groupHelperTextId = groupHelperText.id; - let groupError = this.element.querySelector('.hds-form-group__error'); - let groupErrorId = groupError.id; - let fieldHelperText = this.element.querySelector( - '.hds-form-field__helper-text', - ); - let fieldHelperTextId = fieldHelperText.id; - let fieldError = this.element.querySelector('.hds-form-field__error'); - let fieldErrorId = fieldError.id; - assert - .dom('input') - .hasAttribute( - 'aria-describedby', - `${fieldHelperTextId} ${fieldErrorId} ${groupHelperTextId} ${groupErrorId}`, - ); - }); - - test('it automatically provides all the ID relations between the elements even when Error is conditionally rendered', async function (assert) { - await render( - hbs` - This is the legend - This is the group helper text - - This is the control label - This is the control helper text - This is the control error - - {{#if this.showErrors}} - This is the group error - {{/if}} - `, - ); - - this.set('showErrors', true); - await settled(); - - // the IDs are dynamically generated - let groupHelperText = this.element.querySelector( - '.hds-form-group__helper-text', - ); - let groupHelperTextId = groupHelperText.id; - let groupError = this.element.querySelector('.hds-form-group__error'); - let groupErrorId = groupError.id; - let fieldHelperText = this.element.querySelector( - '.hds-form-field__helper-text', - ); - let fieldHelperTextId = fieldHelperText.id; - let fieldError = this.element.querySelector('.hds-form-field__error'); - let fieldErrorId = fieldError.id; - assert - .dom('input') - .hasAttribute( - 'aria-describedby', - `${fieldHelperTextId} ${fieldErrorId} ${groupHelperTextId} ${groupErrorId}`, - ); - }); - - // NAME - - test('it renders the defined name on all controls within a group', async function (assert) { - await render( - hbs` - Choose datacenter - - NYC1 - - - DC1 - - `, - ); - assert - .dom('[data-test="first-control"]') - .hasAttribute('name', 'datacenter-demo'); - assert - .dom('[data-test="second-control"]') - .hasAttribute('name', 'datacenter-demo'); - }); - - // REQUIRED AND OPTIONAL - - test('it should append an indicator to the legend text and set the required attribute when user input is required', async function (assert) { - await render( - hbs` - This is the legend - - This is the control label - - `, - ); - assert.dom('legend .hds-form-indicator').exists(); - assert.dom('legend .hds-form-indicator').hasText('Required'); - assert.dom('input').hasAttribute('required'); - }); - test('it should append an indicator to the legend text when user input is optional', async function (assert) { - await render( - hbs` - This is the legend - - This is the control label - - `, - ); - assert.dom('legend .hds-form-indicator').exists(); - assert.dom('legend .hds-form-indicator').hasText('(Optional)'); - }); -}); diff --git a/showcase/tests/integration/components/hds/form/divider/index-test.js b/showcase/tests/integration/components/hds/form/divider/index-test.gts similarity index 71% rename from showcase/tests/integration/components/hds/form/divider/index-test.js rename to showcase/tests/integration/components/hds/form/divider/index-test.gts index c9cc49e5423..bfe60295bf7 100644 --- a/showcase/tests/integration/components/hds/form/divider/index-test.js +++ b/showcase/tests/integration/components/hds/form/divider/index-test.gts @@ -4,15 +4,19 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormSeparator } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/separator/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-separator').hasClass('hds-form__separator'); }); @@ -20,7 +24,9 @@ module('Integration | Component | hds/form/separator/index', function (hooks) { // isFullWidth test(`it should have the default max-width if no @isFullWidth prop is declared`, async function (assert) { - await render(hbs``); + await render( + , + ); assert .dom('#test-form-separator') .doesNotHaveClass('hds-form-content--is-full-width'); @@ -28,7 +34,9 @@ module('Integration | Component | hds/form/separator/index', function (hooks) { test(`if @isFullWidth is true, it should not have a max-width set`, async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-separator') diff --git a/showcase/tests/integration/components/hds/form/error/index-test.js b/showcase/tests/integration/components/hds/form/error/index-test.gts similarity index 66% rename from showcase/tests/integration/components/hds/form/error/index-test.js rename to showcase/tests/integration/components/hds/form/error/index-test.gts index 5e368152e23..460b25f780f 100644 --- a/showcase/tests/integration/components/hds/form/error/index-test.js +++ b/showcase/tests/integration/components/hds/form/error/index-test.gts @@ -4,20 +4,24 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormError } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/error/index', function (hooks) { setupRenderingTest(hooks); test('it should render with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-error').hasClass('hds-form-error'); }); test('it should render with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-error').hasClass('my-class'); }); @@ -26,13 +30,18 @@ module('Integration | Component | hds/form/error/index', function (hooks) { test('it renders an error with the defined text', async function (assert) { await render( - hbs`This is the error`, + , ); assert.dom('#test-form-error').hasText('This is the error'); }); test('it renders an error with the yielded content', async function (assert) { await render( - hbs`
This is an HTML element inside the error
`, + , ); assert.dom('#test-form-error pre').exists(); assert @@ -41,7 +50,10 @@ module('Integration | Component | hds/form/error/index', function (hooks) { }); test('it renders multiple error messages as contextual components', async function (assert) { await render( - hbs`First error messageSecond error message`, + , ); assert .dom('#test-form-error .hds-form-error__message') @@ -55,7 +67,9 @@ module('Integration | Component | hds/form/error/index', function (hooks) { test('it renders an error with the correct "id" attribute if the @controlId argument is provided', async function (assert) { await render( - hbs`This is the error`, + , ); assert.dom('#error-my-control-id').exists(); }); diff --git a/showcase/tests/integration/components/hds/form/field/index-test.js b/showcase/tests/integration/components/hds/form/field/index-test.gts similarity index 66% rename from showcase/tests/integration/components/hds/form/field/index-test.js rename to showcase/tests/integration/components/hds/form/field/index-test.gts index 945ffa75f29..65c1a515396 100644 --- a/showcase/tests/integration/components/hds/form/field/index-test.js +++ b/showcase/tests/integration/components/hds/form/field/index-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; +import { render, resetOnerror, setupOnerror, find } from '@ember/test-helpers'; + +import { HdsFormField } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/form/field/index', function (hooks) { setupRenderingTest(hooks); @@ -17,7 +19,9 @@ module('Integration | Component | hds/form/field/index', function (hooks) { test('it should render the component with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-field').hasClass('my-class'); }); @@ -26,54 +30,54 @@ module('Integration | Component | hds/form/field/index', function (hooks) { test('it should render the correct CSS layout class depending on the @layout prop', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-field').hasClass('hds-form-field--layout-vertical'); }); test('it should render the correct DOM order when the @layout prop has value vertical', async function (assert) { await render( - hbs` - This is the label - This is the helper text - `, - ); - let control = this.element.querySelector( - '#test-form-field .hds-form-field__control', - ); - let helperText = this.element.querySelector( - '#test-form-field .hds-form-field__helper-text', + , ); - assert.equal(control.previousElementSibling, helperText); + const control = find('#test-form-field .hds-form-field__control'); + const helperText = find('#test-form-field .hds-form-field__helper-text'); + assert.equal(control?.previousElementSibling, helperText); }); test('it should render the correct DOM order when the @layout prop has value flag', async function (assert) { await render( - hbs` - This is the label - This is the helper text - `, - ); - let control = this.element.querySelector( - '#test-form-field .hds-form-field__control', - ); - let helperText = this.element.querySelector( - '#test-form-field .hds-form-field__helper-text', + , ); - assert.equal(control.nextElementSibling, helperText); + const control = find('#test-form-field .hds-form-field__control'); + const helperText = find('#test-form-field .hds-form-field__helper-text'); + assert.equal(control?.nextElementSibling, helperText); }); // YIELDED (CONTEXTUAL) COMPONENTS test('it renders the yielded contextual components', async function (assert) { await render( - hbs` + , ); assert.dom('#test-form-field .hds-form-field__label').exists(); assert.dom('.hds-form-field__label').hasText('This is the label'); @@ -89,19 +93,22 @@ module('Integration | Component | hds/form/field/index', function (hooks) { }); test('it automatically provides all the ID relations between the elements', async function (assert) { await render( - hbs` + , ); // the control ID is dynamically generated - let control = this.element.querySelector( - '#test-form-field .hds-form-field__control pre', - ); - let controlId = control.id; + const control = find('#test-form-field .hds-form-field__control pre'); + const controlId = control?.id ?? ''; assert.dom('.hds-form-field__label').hasAttribute('for', controlId); assert .dom('.hds-form-field__helper-text') @@ -121,15 +128,25 @@ module('Integration | Component | hds/form/field/index', function (hooks) { }); test('it automatically provides all the ID relations between the elements with a custom @id', async function (assert) { await render( - hbs` + , ); - let controlId = 'my-custom-id'; + const controlId = 'my-custom-id'; assert.dom('.hds-form-field__label').hasAttribute('for', controlId); assert .dom('.hds-form-field__label') @@ -152,19 +169,27 @@ module('Integration | Component | hds/form/field/index', function (hooks) { }); test('it provides all the ID relations between the elements and allows extra `aria-describedby` attributes', async function (assert) { await render( - hbs` + , ); // the control ID is dynamically generated - let control = this.element.querySelector( - '#test-form-field .hds-form-field__control pre', - ); - let controlId = control.id; + const control = find('#test-form-field .hds-form-field__control pre'); + const controlId = control?.id ?? ''; assert.dom('.hds-form-field__label').hasAttribute('for', controlId); assert .dom('.hds-form-field__helper-text') @@ -187,18 +212,22 @@ module('Integration | Component | hds/form/field/index', function (hooks) { test('it should append an indicator to the label text when user input is required', async function (assert) { await render( - hbs` - This is the label - `, + , ); assert.dom('label .hds-form-indicator').exists(); assert.dom('label .hds-form-indicator').hasText('Required'); }); test('it should append an indicator to the label text when user input is optional', async function (assert) { await render( - hbs` - This is the label - `, + , ); assert.dom('label .hds-form-indicator').exists(); assert.dom('label .hds-form-indicator').hasText('(Optional)'); @@ -213,7 +242,12 @@ module('Integration | Component | hds/form/field/index', function (hooks) { setupOnerror(function (error) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); }); diff --git a/showcase/tests/integration/components/hds/form/fieldset/index-test.js b/showcase/tests/integration/components/hds/form/fieldset/index-test.gts similarity index 69% rename from showcase/tests/integration/components/hds/form/fieldset/index-test.js rename to showcase/tests/integration/components/hds/form/fieldset/index-test.gts index 580ab2f2941..12b70fbfafe 100644 --- a/showcase/tests/integration/components/hds/form/fieldset/index-test.js +++ b/showcase/tests/integration/components/hds/form/fieldset/index-test.gts @@ -4,19 +4,25 @@ */ import { module, test } from 'qunit'; +import { render, find } from '@ember/test-helpers'; + +import { HdsFormFieldset } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/form/fieldset/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with an appropriate CSS class', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-fieldset').hasClass('hds-form-group'); }); test('it renders the element as
', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-fieldset').hasTagName('fieldset'); }); @@ -24,7 +30,9 @@ module('Integration | Component | hds/form/fieldset/index', function (hooks) { test('it should render the correct CSS layout class depending on the @layout prop', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-fieldset') @@ -35,12 +43,16 @@ module('Integration | Component | hds/form/fieldset/index', function (hooks) { test('it renders the yielded contextual components', async function (assert) { await render( - hbs` + , ); assert.dom('#test-form-fieldset .hds-form-group__legend').exists(); assert.dom('.hds-form-group__legend').hasText('This is the legend'); @@ -57,16 +69,22 @@ module('Integration | Component | hds/form/fieldset/index', function (hooks) { }); test('it automatically provides IDs for helper text and error', async function (assert) { await render( - hbs` + , ); // the fieldset ID is dynamically generated - let fieldset = this.element.querySelector('fieldset'); - let fieldsetId = fieldset.id; + const fieldset = find('fieldset'); + const fieldsetId = fieldset?.id; assert .dom('.hds-form-group__helper-text') .hasAttribute('id', `helper-text-${fieldsetId}`); @@ -79,18 +97,22 @@ module('Integration | Component | hds/form/fieldset/index', function (hooks) { test('it should append an indicator to the legend text when user input is required', async function (assert) { await render( - hbs` + , ); assert.dom('legend .hds-form-indicator').exists(); assert.dom('legend .hds-form-indicator').hasText('Required'); }); test('it should append an indicator to the legend text when user input is optional', async function (assert) { await render( - hbs` + , ); assert.dom('legend .hds-form-indicator').exists(); assert.dom('legend .hds-form-indicator').hasText('(Optional)'); diff --git a/showcase/tests/integration/components/hds/form/file-input/base-test.js b/showcase/tests/integration/components/hds/form/file-input/base-test.gts similarity index 69% rename from showcase/tests/integration/components/hds/form/file-input/base-test.js rename to showcase/tests/integration/components/hds/form/file-input/base-test.gts index b989b85d5fa..90a3414c3bb 100644 --- a/showcase/tests/integration/components/hds/form/file-input/base-test.js +++ b/showcase/tests/integration/components/hds/form/file-input/base-test.gts @@ -4,21 +4,30 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormFileInputBase } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/file-input/base', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-file-input').hasClass('hds-form-file-input'); }); test('it should set aria-describedby and id arguments if pass @id or @ariaDescribedBy', async function (assert) { await render( - hbs``, + , ); assert .dom('#custom-id') diff --git a/showcase/tests/integration/components/hds/form/file-input/field-test.js b/showcase/tests/integration/components/hds/form/file-input/field-test.gts similarity index 70% rename from showcase/tests/integration/components/hds/form/file-input/field-test.js rename to showcase/tests/integration/components/hds/form/file-input/field-test.gts index 5efbe685d44..1d3854409c2 100644 --- a/showcase/tests/integration/components/hds/form/file-input/field-test.js +++ b/showcase/tests/integration/components/hds/form/file-input/field-test.gts @@ -4,9 +4,12 @@ */ import { module, test } from 'qunit'; +import { render, resetOnerror, settled, find } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormFileInputField } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, resetOnerror, settled } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | hds/form/file-input/field', function (hooks) { setupRenderingTest(hooks); @@ -16,14 +19,14 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { }); test('it should render the component with a specific CSS class', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-form-field__control').exists(); }); // ID test('it should render the input with a custom @id', async function (assert) { - await render(hbs``); + await render(); assert.dom('input').hasAttribute('id', 'my-input'); }); @@ -31,11 +34,13 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { test('it renders the yielded contextual components', async function (assert) { await render( - hbs` + , ); assert.dom('.hds-form-field__label').exists(); assert.dom('.hds-form-field__helper-text').exists(); @@ -44,7 +49,7 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { }); test('it does not render the yielded contextual components if not provided', async function (assert) { - await render(hbs``); + await render(); assert.dom('.hds-form-field__label').doesNotExist(); assert.dom('.hds-form-field__helper-text').doesNotExist(); assert.dom('.hds-form-field__error').doesNotExist(); @@ -52,15 +57,17 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { test('it automatically provides all the ID relations between the elements', async function (assert) { await render( - hbs` + , ); // the control ID is dynamically generated - let control = this.element.querySelector('.hds-form-field__control input'); - let controlId = control.id; + const control = find('.hds-form-field__control input'); + const controlId = control?.id ?? ''; assert.dom('.hds-form-field__label').hasAttribute('for', controlId); assert .dom('.hds-form-field__helper-text') @@ -76,22 +83,28 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { .hasAttribute('id', `error-${controlId}`); }); test('it automatically provides all the ID relations between the elements when dynamically rendered', async function (assert) { + const context = new TrackedObject({ + showErrors: false, + }); + await render( - hbs` + , ); - this.set('showErrors', true); + context.showErrors = true; await settled(); // the control ID is dynamically generated - let control = this.element.querySelector('.hds-form-field__control input'); - let controlId = control.id; + const control = find('.hds-form-field__control input'); + const controlId = control?.id ?? ''; assert.dom('.hds-form-field__label').hasAttribute('for', controlId); assert .dom('.hds-form-field__helper-text') @@ -111,9 +124,11 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { test('it should append an indicator to the label text and set the required attribute when user input is required', async function (assert) { await render( - hbs` - This is the label - `, + , ); assert.dom('label .hds-form-indicator').exists(); assert.dom('label .hds-form-indicator').hasText('Required'); @@ -122,9 +137,11 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { test('it should append an indicator to the label text when user input is optional', async function (assert) { await render( - hbs` - This is the label - `, + , ); assert.dom('label .hds-form-indicator').exists(); assert.dom('label .hds-form-indicator').hasText('(Optional)'); @@ -132,9 +149,11 @@ module('Integration | Component | hds/form/file-input/field', function (hooks) { test('it should not append an indicator to the label text when the required attribute is set', async function (assert) { await render( - hbs` - This is the label - `, + , ); assert.dom('input').hasAttribute('required'); assert.dom('label .hds-form-indicator').doesNotExist(); diff --git a/showcase/tests/integration/components/hds/form/footer/index-test.js b/showcase/tests/integration/components/hds/form/footer/index-test.gts similarity index 72% rename from showcase/tests/integration/components/hds/form/footer/index-test.js rename to showcase/tests/integration/components/hds/form/footer/index-test.gts index f05d3e7c66b..4d46bec42de 100644 --- a/showcase/tests/integration/components/hds/form/footer/index-test.js +++ b/showcase/tests/integration/components/hds/form/footer/index-test.gts @@ -4,15 +4,17 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormFooter } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/footer/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-footer').hasClass('hds-form__footer'); }); @@ -20,7 +22,9 @@ module('Integration | Component | hds/form/footer/index', function (hooks) { test('it should yield the Footer children', async function (assert) { await render( - hbs`
test
`, + , ); assert .dom('#test-form-footer > pre') @@ -33,7 +37,7 @@ module('Integration | Component | hds/form/footer/index', function (hooks) { // isFullWidth test(`it should have the default max-width if no @isFullWidth prop is declared`, async function (assert) { - await render(hbs``); + await render(); assert .dom('#test-form-footer') .doesNotHaveClass('hds-form-content--is-full-width'); @@ -41,7 +45,9 @@ module('Integration | Component | hds/form/footer/index', function (hooks) { test(`if @isFullWidth is true, it should not have a max-width set`, async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-footer').hasClass('hds-form-content--is-full-width'); }); diff --git a/showcase/tests/integration/components/hds/form/header/description/index-test.js b/showcase/tests/integration/components/hds/form/header/description/index-test.gts similarity index 69% rename from showcase/tests/integration/components/hds/form/header/description/index-test.js rename to showcase/tests/integration/components/hds/form/header/description/index-test.gts index a90ff7fd548..aa028101014 100644 --- a/showcase/tests/integration/components/hds/form/header/description/index-test.js +++ b/showcase/tests/integration/components/hds/form/header/description/index-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormHeaderDescription } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/header/description/index', @@ -15,7 +17,9 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-description') @@ -26,7 +30,10 @@ module( test('it should render the yielded content', async function (assert) { await render( - hbs`
test
`, + , ); assert.dom('#test-form-description > pre').exists().hasText('test'); }); diff --git a/showcase/tests/integration/components/hds/form/header/index-test.js b/showcase/tests/integration/components/hds/form/header/index-test.gts similarity index 72% rename from showcase/tests/integration/components/hds/form/header/index-test.js rename to showcase/tests/integration/components/hds/form/header/index-test.gts index 7f379bd96ce..b7fe699618a 100644 --- a/showcase/tests/integration/components/hds/form/header/index-test.js +++ b/showcase/tests/integration/components/hds/form/header/index-test.gts @@ -4,15 +4,17 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormHeader } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/header/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-header').hasClass('hds-form__header'); }); @@ -20,7 +22,10 @@ module('Integration | Component | hds/form/header/index', function (hooks) { test('it should yield the Title and Description children', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-header > .hds-form__header-title') @@ -34,7 +39,7 @@ module('Integration | Component | hds/form/header/index', function (hooks) { // isFullWidth test(`it should have the default max-width if no @isFullWidth prop is declared`, async function (assert) { - await render(hbs``); + await render(); assert .dom('#test-form-header') .doesNotHaveClass('hds-form-content--is-full-width'); @@ -42,7 +47,9 @@ module('Integration | Component | hds/form/header/index', function (hooks) { test(`if @isFullWidth is true, it should not have a max-width set`, async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-header').hasClass('hds-form-content--is-full-width'); }); diff --git a/showcase/tests/integration/components/hds/form/header/title/index-test.js b/showcase/tests/integration/components/hds/form/header/title/index-test.gts similarity index 71% rename from showcase/tests/integration/components/hds/form/header/title/index-test.js rename to showcase/tests/integration/components/hds/form/header/title/index-test.gts index b0310d090bc..3a494996f82 100644 --- a/showcase/tests/integration/components/hds/form/header/title/index-test.js +++ b/showcase/tests/integration/components/hds/form/header/title/index-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormHeaderTitle } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/header/title/index', @@ -15,7 +17,7 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-header-title').hasClass('hds-form__header-title'); }); @@ -23,7 +25,10 @@ module( // CONTENT test('it should render the yielded content', async function (assert) { await render( - hbs`
test
`, + , ); assert.dom('#test-form-header-title > pre').exists().hasText('test'); }); @@ -33,14 +38,16 @@ module( // Tag test('it should render the component using the default div tag', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-header-title').hasTagName('div'); }); test('it should render the component using the specified tag', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-header-title').hasTagName('h2'); }); @@ -48,7 +55,7 @@ module( // Size test('it should render the component with the default size', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-header-title') @@ -57,7 +64,9 @@ module( test('it should render the component with a specified size', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-header-title') @@ -75,7 +84,12 @@ module( assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); diff --git a/showcase/tests/integration/components/hds/form/helper-text/index-test.js b/showcase/tests/integration/components/hds/form/helper-text/index-test.gts similarity index 65% rename from showcase/tests/integration/components/hds/form/helper-text/index-test.js rename to showcase/tests/integration/components/hds/form/helper-text/index-test.gts index 2a987d8df99..35c7413126c 100644 --- a/showcase/tests/integration/components/hds/form/helper-text/index-test.js +++ b/showcase/tests/integration/components/hds/form/helper-text/index-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormHelperText } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/helper-text/index', @@ -14,12 +16,19 @@ module( setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-helper-text').hasClass('hds-form-helper-text'); }); test('it should render with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-helper-text').hasClass('my-class'); }); @@ -28,13 +37,18 @@ module( test('it renders a helper text with the defined text', async function (assert) { await render( - hbs`This is the helper text`, + , ); assert.dom('#test-form-helper-text').hasText('This is the helper text'); }); test('it renders a helper text with the yielded content', async function (assert) { await render( - hbs`
This is an HTML element inside the helper text
`, + , ); assert.dom('#test-form-helper-text > pre').exists(); assert @@ -46,7 +60,9 @@ module( test('it renders a helper text with the correct "id" attribute if the @controlId argument is provided', async function (assert) { await render( - hbs`This is the helper text`, + , ); assert.dom('#helper-text-my-control-id').exists(); }); diff --git a/showcase/tests/integration/components/hds/form/index-test.js b/showcase/tests/integration/components/hds/form/index-test.gts similarity index 75% rename from showcase/tests/integration/components/hds/form/index-test.js rename to showcase/tests/integration/components/hds/form/index-test.gts index d54dfd194de..01bb17360a6 100644 --- a/showcase/tests/integration/components/hds/form/index-test.js +++ b/showcase/tests/integration/components/hds/form/index-test.gts @@ -6,15 +6,15 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, setupOnerror } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { HdsForm } from '@hashicorp/design-system-components/components'; import { AVAILABLE_TAGS } from '@hashicorp/design-system-components/components/hds/form/index'; module('Integration | Component | hds/form/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form').hasClass('hds-form'); }); @@ -23,12 +23,14 @@ module('Integration | Component | hds/form/index', function (hooks) { // Tag test('it should render the component using a form tag by default', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-component').hasTagName('form'); }); test('it should render the component using a div tag if specified in the @tag prop', async function (assert) { - await render(hbs``); + await render( + , + ); assert.dom('#test-form-component').hasTagName('div'); }); @@ -36,7 +38,9 @@ module('Integration | Component | hds/form/index', function (hooks) { test('it should set an inline style for the section max-width custom property', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-component').hasStyle( { @@ -50,25 +54,26 @@ module('Integration | Component | hds/form/index', function (hooks) { test('it should yield the different subcomponents as children, for the different available tags', async function (assert) { for (const tag of AVAILABLE_TAGS) { - this.set('tag', tag); await render( - hbs` - - - - - - - - - - - - - - - - `, + , ); // Form Header content @@ -135,7 +140,12 @@ module('Integration | Component | hds/form/index', function (hooks) { assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); }); - await render(hbs``); + await render( + , + ); assert.throws(function () { throw new Error(errorMessage); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.js b/showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.gts similarity index 58% rename from showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.js rename to showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.gts index e33822949dd..c328e7761f2 100644 --- a/showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.js +++ b/showcase/tests/integration/components/hds/form/key-value-inputs/add-row-button-test.gts @@ -4,9 +4,12 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render, click } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormKeyValueInputsAddRowButton } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/key-value-inputs/add-row-button', @@ -15,7 +18,11 @@ module( test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-key-value-add-row-button') @@ -26,14 +33,23 @@ module( test('it should render with default text', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-key-value-add-row-button').hasText('Add row'); }); test('it should render text from `@text` argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-key-value-add-row-button').hasText('Custom text'); }); @@ -41,24 +57,36 @@ module( // CALLBACKS test('it should call `@onClick` action when clicked', async function (assert) { - let clicked = false; - this.set('onClick', () => { - clicked = true; + const context = new TrackedObject({ + isClicked: false, }); + const onClick = () => { + context.isClicked = true; + }; + await render( - hbs``, + , ); await click('#test-form-key-value-add-row-button'); - assert.ok(clicked); + assert.ok(context.isClicked); }); // ACCESSIBILITY test('it should provide an `aria-description` attribute', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-key-value-add-row-button') diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.gts b/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.gts new file mode 100644 index 00000000000..2c7c867950b --- /dev/null +++ b/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.gts @@ -0,0 +1,210 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { render, click, fillIn, settled } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { + HdsFormKeyValueInputs, + HdsFormKeyValueInputsDeleteRowButton, +} from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +module( + 'Integration | Component | hds/form/key-value-inputs/delete-row-button', + function (hooks) { + setupRenderingTest(hooks); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + const rowData = { test: true }; + await render( + , + ); + assert + .dom('#test-form-key-value-delete-row-button') + .hasClass('hds-form-key-value-inputs__delete-row-button'); + }); + + // TEXT + + test('it should render with default text', async function (assert) { + const rowData = { test: true }; + await render( + , + ); + assert + .dom('#test-form-key-value-delete-row-button') + .hasAria('label', 'Delete row 1'); + }); + + test('it should render text from `@text` argument', async function (assert) { + const rowData = { test: true }; + await render( + , + ); + assert + .dom('#test-form-key-value-delete-row-button') + .hasAria('label', 'Custom text'); + }); + + // CALLBACKS + + test('it should call `@onClick` action when clicked and return rowData/rowIndex as positional arguments', async function (assert) { + const rowData = { test: true }; + const rowIndex = 5; + + const context = new TrackedObject<{ + isClicked: boolean; + passedRowData: unknown; + passedRowIndex: number | undefined; + }>({ + isClicked: false, + passedRowData: undefined, + passedRowIndex: undefined, + }); + + const onClick = (rowData: unknown, rowIndex: number) => { + context.isClicked = true; + context.passedRowData = rowData; + context.passedRowIndex = rowIndex; + }; + + await render( + , + ); + + await click('#test-form-key-value-delete-row-button'); + assert.ok(context.isClicked); + assert.strictEqual( + context.passedRowData, + rowData, + 'rowData is passed as first argument', + ); + assert.strictEqual( + context.passedRowIndex, + rowIndex, + 'rowIndex is passed as second argument', + ); + }); + + test('it should call `@onInsert/@onRemove` callbacks when added/removed', async function (assert) { + const context = new TrackedObject({ + isRendered: false, + isInserted: false, + isRemoved: false, + }); + const onInsert = () => { + context.isInserted = true; + }; + const onRemove = () => { + context.isRemoved = true; + }; + + await render( + , + ); + + assert.notOk(context.isInserted); + assert.notOk(context.isRemoved); + + context.isRendered = true; + await settled(); + assert.ok(context.isInserted); + + context.isRendered = false; + await settled(); + assert.ok(context.isRemoved); + }); + + // RETURN FOCUS + + test('it returns focus to the main frameset when a row is deleted and the `DeleteRowButton` element removed from the DOM', async function (assert) { + const context = new TrackedObject({ + data: [ + { key: 'Test key', value: 'Test value' }, + { key: 'Another key', value: 'Another value' }, + ], + }); + + const onClick = function ( + _passedRowData: unknown, + passedRowIndex: number, + ) { + context.data = context.data.filter( + (_row, index) => index !== passedRowIndex, + ); + }; + + await render( + , + ); + + const inputSelector = + '#test-form-key-value-inputs [data-test-input="row-1"]'; + const buttonSelector = + '#test-form-key-value-inputs [data-test-button="row-1"]'; + await fillIn(inputSelector, 'test'); + assert.dom(inputSelector).isFocused(); + await click(buttonSelector); + assert.dom('#test-form-key-value-inputs').isFocused(); + }); + }, +); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.js b/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.js deleted file mode 100644 index baeda483fdb..00000000000 --- a/showcase/tests/integration/components/hds/form/key-value-inputs/delete-row-button-test.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render, click, fillIn } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module( - 'Integration | Component | hds/form/key-value-inputs/delete-row-button', - function (hooks) { - setupRenderingTest(hooks); - - test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render( - hbs``, - ); - assert - .dom('#test-form-key-value-delete-row-button') - .hasClass('hds-form-key-value-inputs__delete-row-button'); - }); - - // TEXT - - test('it should render with default text', async function (assert) { - await render( - hbs``, - ); - assert - .dom('#test-form-key-value-delete-row-button') - .hasAria('label', 'Delete row 1'); - }); - - test('it should render text from `@text` argument', async function (assert) { - await render( - hbs``, - ); - assert - .dom('#test-form-key-value-delete-row-button') - .hasAria('label', 'Custom text'); - }); - - // CALLBACKS - - test('it should call `@onClick` action when clicked and return rowData/rowIndex as positional arguments', async function (assert) { - const rowData = { test: true }; - const rowIndex = 5; - this.set('rowData', rowData); - this.set('rowIndex', rowIndex); - let clicked = false; - this.set( - 'onClick', - function (passedRowData, passedRowIndex) { - clicked = true; - this.set('passedRowData', passedRowData); - this.set('passedRowIndex', passedRowIndex); - }.bind(this), - ); - - await render( - hbs``, - ); - - await click('#test-form-key-value-delete-row-button'); - assert.ok(clicked); - assert.strictEqual( - this.passedRowData, - rowData, - 'rowData is passed as first argument', - ); - assert.strictEqual( - this.passedRowIndex, - rowIndex, - 'rowIndex is passed as second argument', - ); - }); - - test('it should call `@onInsert/@onRemove` callbacks when added/removed', async function (assert) { - this.set('isRendered', false); - let inserted = false; - let removed = false; - this.set('onInsert', () => { - inserted = true; - }); - this.set('onRemove', () => { - removed = true; - }); - - await render( - hbs` - {{#if this.isRendered}} - - {{/if}} - `, - ); - - assert.notOk(inserted); - assert.notOk(removed); - this.set('isRendered', true); - assert.ok(inserted); - this.set('isRendered', false); - assert.ok(removed); - }); - - // RETURN FOCUS - - test('it returns focus to the main frameset when a row is deleted and the `DeleteRowButton` element removed from the DOM', async function (assert) { - this.data = [ - { key: 'Test key', value: 'Test value' }, - { key: 'Another key', value: 'Another value' }, - ]; - this.set( - 'onClick', - function (_passedRowData, passedRowIndex) { - this.set( - 'data', - this.data.filter((_row, idx) => idx !== passedRowIndex), - ); - }.bind(this), - ); - - await render(hbs` - - <:row as |R|> - {{#let R.rowIndex as |index|}} - - Label - - - - - {{/let}} - - - `); - - const inputSelector = - '#test-form-key-value-inputs [data-test-input="row-1"]'; - const buttonSelector = - '#test-form-key-value-inputs [data-test-button="row-1"]'; - await fillIn(inputSelector, 'test'); - assert.dom(inputSelector).isFocused(); - await click(buttonSelector); - assert.dom('#test-form-key-value-inputs').isFocused(); - }); - }, -); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/field-test.js b/showcase/tests/integration/components/hds/form/key-value-inputs/field-test.gts similarity index 52% rename from showcase/tests/integration/components/hds/form/key-value-inputs/field-test.js rename to showcase/tests/integration/components/hds/form/key-value-inputs/field-test.gts index 0295a4eccba..bdbd841ec37 100644 --- a/showcase/tests/integration/components/hds/form/key-value-inputs/field-test.js +++ b/showcase/tests/integration/components/hds/form/key-value-inputs/field-test.gts @@ -4,9 +4,14 @@ */ import { module, test } from 'qunit'; +import { eq } from 'ember-truth-helpers'; +import { render, find, settled } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormKeyValueInputsField } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import NOOP from 'showcase/utils/noop'; const YIELDED_INPUTS = [ { type: 'FileInput', selector: '.hds-form-file-input' }, @@ -18,75 +23,81 @@ const YIELDED_INPUTS = [ { type: 'Textarea', selector: '.hds-form-textarea' }, ]; +const createKeyValueInputsField = async (options: { + type?: string; + isInvalid?: boolean; + id?: string; + extraAriaDescribedBy?: string; +}) => { + const type = options.type ?? 'TextInput'; + const superSelectOptions = ['Option 1', 'Option 2']; + const superSelectSelected = 'Option 1'; + + await render( + , + ); +}; + module( 'Integration | Component | hds/form/key-value-inputs/field', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set('createKeyValueInputsField', async (args = {}) => { - this.type = args.type ?? 'TextInput'; - this.isInvalid = args.isInvalid; - this.controlId = args.id; - this.extraAriaDescribedBy = args.extraAriaDescribedBy; - // --- - this.options = ['Option 1', 'Option 2']; - this.selected_option = 'Option 1'; - this.NOOP = () => {}; - - await render(hbs` - - Label - Helper text - {{#if (eq this.type 'FileInput')}} - - {{/if}} - {{#if (eq this.type 'MaskedInput')}} - - {{/if}} - {{#if (eq this.type 'SuperSelectSingle')}} - - {{option}} - - {{/if}} - {{#if (eq this.type 'SuperSelectMultiple')}} - - {{option}} - - {{/if}} - {{#if (eq this.type 'Select')}} - - {{/if}} - {{#if (eq this.type 'TextInput')}} - - {{/if}} - {{#if (eq this.type 'Textarea')}} - - {{/if}} - Error text - - `); - }); - }); - test('it should render the component with a CSS class that matches the component name', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-key-value-field') @@ -96,11 +107,17 @@ module( // LABEL HIDDEN TEXT test('it should render the label with screen-reader-only text based on the provided `@rowIndex` argument', async function (assert) { - await render(hbs` - - Label - - `); + await render( + , + ); assert .dom( @@ -118,11 +135,18 @@ module( // REQUIRED/OPTIONAL test('it should render as required with `@isRequired` argument', async function (assert) { - await render(hbs` - - Label - - `); + await render( + , + ); assert .dom( @@ -132,11 +156,18 @@ module( }); test('it should render as optional with `@isOptional` argument', async function (assert) { - await render(hbs` - - Label - - `); + await render( + , + ); assert .dom( @@ -148,7 +179,7 @@ module( // YIELDED (CONTEXTUAL) COMPONENTS test('it renders the yielded `Label`, `HelperText`, and `Error` contextual components', async function (assert) { - await this.createKeyValueInputsField(); + await createKeyValueInputsField({}); assert .dom( '#test-form-key-value-field .hds-form-key-value-inputs__field-label', @@ -168,7 +199,7 @@ module( YIELDED_INPUTS.forEach(({ type, selector }) => { test(`it renders the yielded "${type}" input as contextual components`, async function (assert) { - await this.createKeyValueInputsField({ type }); + await createKeyValueInputsField({ type }); assert.dom(`#test-form-key-value-field ${selector}`).exists(); }); }); @@ -179,7 +210,7 @@ module( if (type !== 'FileInput') { // file input doesn't have an invalid state test(`it should render the "${type}" input as invalid if \`@isInvalid\` is true`, async function (assert) { - await this.createKeyValueInputsField({ type, isInvalid: true }); + await createKeyValueInputsField({ type, isInvalid: true }); assert .dom(`#test-form-key-value-field ${selector}`) // we need to remove the initial `.` from the selector string @@ -192,7 +223,13 @@ module( test('it should add `data-width` if `@width` is provided', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-key-value-field') @@ -202,30 +239,40 @@ module( // CALLBACKS test('it should call `@onInsert/@onRemove` callbacks when added/removed', async function (assert) { - this.set('isRendered', false); - let inserted = false; - let removed = false; - this.set('onInsert', () => { - inserted = true; - }); - this.set('onRemove', () => { - removed = true; + const context = new TrackedObject({ + isRendered: false, + isInserted: false, + isRemoved: false, }); + const onInsert = () => { + context.isInserted = true; + }; + const onRemove = () => { + context.isRemoved = true; + }; await render( - hbs` - {{#if this.isRendered}} - + , ); - assert.notOk(inserted); - assert.notOk(removed); - this.set('isRendered', true); - assert.ok(inserted); - this.set('isRendered', false); - assert.ok(removed); + assert.notOk(context.isInserted); + assert.notOk(context.isRemoved); + + context.isRendered = true; + await settled(); + assert.ok(context.isInserted); + + context.isRendered = false; + await settled(); + assert.ok(context.isRemoved); }); // ACCESSIBILITY @@ -234,46 +281,46 @@ module( ['default', 'custom'].forEach((mode) => { test(`it should associate the label and help text appropriately for the "${type}" input - ${mode === 'custom' ? 'with custom @id' : 'default'}`, async function (assert) { const extraAriaDescribedBy = 'extra'; - const opts = { type, extraAriaDescribedBy }; + const opts: { + type: string; + extraAriaDescribedBy: string; + id?: string; + } = { type, extraAriaDescribedBy }; if (mode === 'custom') { opts.id = 'custom-id'; } - await this.createKeyValueInputsField(opts); - - const labelId = document.querySelector( - '#test-form-key-value-field .hds-form-label', - ).id; - const inputId = document.querySelector( - `#test-form-key-value-field ${selector}`, - ).id; - const helperId = document.querySelector( + await createKeyValueInputsField(opts); + + const label = find('#test-form-key-value-field .hds-form-label'); + const input = find(`#test-form-key-value-field ${selector}`); + const helper = find( '#test-form-key-value-field .hds-form-key-value-inputs__field-helper-text', - ).id; - const errorId = document.querySelector( + ); + const error = find( '#test-form-key-value-field .hds-form-key-value-inputs__field-error', - ).id; + ); if (type === 'SuperSelectSingle' || type === 'SuperSelectMultiple') { assert .dom('#test-form-key-value-field [role="combobox"]') - .hasAria('labelledby', labelId); + .hasAria('labelledby', label?.id ?? ''); assert .dom('#test-form-key-value-field [role="combobox"]') .hasAria( 'describedby', - `${helperId} ${errorId} ${extraAriaDescribedBy}`, + `${helper?.id} ${error?.id} ${extraAriaDescribedBy}`, ); } else { assert .dom( '#test-form-key-value-field .hds-form-key-value-inputs__field-label', ) - .hasAttribute('for', inputId); + .hasAttribute('for', input?.id ?? ''); assert .dom(`#test-form-key-value-field ${selector}`) .hasAria( 'describedby', - `${helperId} ${errorId} ${extraAriaDescribedBy}`, + `${helper?.id} ${error?.id} ${extraAriaDescribedBy}`, ); } }); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.gts b/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.gts new file mode 100644 index 00000000000..3b82df1fd5e --- /dev/null +++ b/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.gts @@ -0,0 +1,85 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { render, settled } from '@ember/test-helpers'; +import { TrackedObject } from 'tracked-built-ins'; + +import { HdsFormKeyValueInputsGeneric } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; + +module( + 'Integration | Component | hds/form/key-value-inputs/generic', + function (hooks) { + setupRenderingTest(hooks); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + await render( + , + ); + assert + .dom('#test-form-key-value-generic') + .hasClass('hds-form-key-value-inputs__generic-container'); + }); + + // YIELDING + + test('it should render the content', async function (assert) { + await render( + , + ); + assert + .dom('#test-form-key-value-generic #foo') + .hasText('Generic content'); + }); + + // CALLBACKS + + test('it should call `@onInsert/@onRemove` callbacks when added/removed', async function (assert) { + const context = new TrackedObject({ + isRendered: false, + isInserted: false, + isRemoved: false, + }); + const onInsert = () => { + context.isInserted = true; + }; + const onRemove = () => { + context.isRemoved = true; + }; + + await render( + , + ); + + assert.notOk(context.isInserted); + assert.notOk(context.isRemoved); + + context.isRendered = true; + await settled(); + assert.ok(context.isInserted); + + context.isRendered = false; + await settled(); + assert.ok(context.isRemoved); + }); + }, +); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.js b/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.js deleted file mode 100644 index f94aa3752c3..00000000000 --- a/showcase/tests/integration/components/hds/form/key-value-inputs/generic-test.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -module( - 'Integration | Component | hds/form/key-value-inputs/generic', - function (hooks) { - setupRenderingTest(hooks); - - test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs` - Generic content - `); - assert - .dom('#test-form-key-value-generic') - .hasClass('hds-form-key-value-inputs__generic-container'); - }); - - // YIELDING - - test('it should render the content', async function (assert) { - await render(hbs` - Generic content - `); - assert - .dom('#test-form-key-value-generic #foo') - .hasText('Generic content'); - }); - - // CALLBACKS - - test('it should call `@onInsert/@onRemove` callbacks when added/removed', async function (assert) { - this.set('isRendered', false); - let inserted = false; - let removed = false; - this.set('onInsert', () => { - inserted = true; - }); - this.set('onRemove', () => { - removed = true; - }); - - await render( - hbs` - {{#if this.isRendered}} - - {{/if}} - `, - ); - - assert.notOk(inserted); - assert.notOk(removed); - this.set('isRendered', true); - assert.ok(inserted); - this.set('isRendered', false); - assert.ok(removed); - }); - }, -); diff --git a/showcase/tests/integration/components/hds/form/key-value-inputs/index-test.js b/showcase/tests/integration/components/hds/form/key-value-inputs/index-test.gts similarity index 63% rename from showcase/tests/integration/components/hds/form/key-value-inputs/index-test.js rename to showcase/tests/integration/components/hds/form/key-value-inputs/index-test.gts index 646fc803964..b00649fc827 100644 --- a/showcase/tests/integration/components/hds/form/key-value-inputs/index-test.js +++ b/showcase/tests/integration/components/hds/form/key-value-inputs/index-test.gts @@ -4,63 +4,69 @@ */ import { module, test } from 'qunit'; +import { render, find } from '@ember/test-helpers'; +import { get } from '@ember/helper'; + +import { HdsFormKeyValueInputs } from '@hashicorp/design-system-components/components'; + import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +const createKeyValueInputs = async (options: { + data?: Record[]; + isFieldsetRequired?: boolean; + isFieldsetOptional?: boolean; + extraAriaDescribedBy?: string; +}) => { + const data = options?.data ?? []; + + return await render( + , + ); +}; module( 'Integration | Component | hds/form/key-value-inputs/index', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.set('createKeyValueInputs', async (args = {}) => { - this.data = args.data ?? []; - this.isFieldsetRequired = args.isFieldsetRequired; - this.isFieldsetOptional = args.isFieldsetOptional; - this.extraAriaDescribedBy = args.extraAriaDescribedBy; - return await render(hbs` - - <:header as |H|> - Legend - Helper text - - Generic content - - - - <:row as |R|> - {{!-- for testing purposes, we specifically move it first so we can validate that is rendered last --}} - - - Value - Helper text - - Error text - - Generic content - - - <:footer as |F|> - - - Alert content - - Error text - - - `); - }); - }); - test('it should render the component with a CSS class that matches the component name', async function (assert) { - await this.createKeyValueInputs(); + await createKeyValueInputs({}); assert .dom('#test-form-key-value-inputs') .hasClass('hds-form-key-value-inputs'); @@ -69,7 +75,7 @@ module( // HEADER test('it should render the header content', async function (assert) { - await this.createKeyValueInputs(); + await createKeyValueInputs({}); assert .dom('#test-form-key-value-inputs .hds-form-key-value-inputs__header') .exists(); @@ -90,7 +96,7 @@ module( .hasText('Generic content'); }); test('it should render the "required" indicator in the legend', async function (assert) { - await this.createKeyValueInputs({ isFieldsetRequired: true }); + await createKeyValueInputs({ isFieldsetRequired: true }); assert .dom( '#test-form-key-value-inputs .hds-form-key-value-inputs__header legend .hds-form-indicator.hds-badge--color-neutral', @@ -98,7 +104,7 @@ module( .exists(); }); test('it should render the "optional" indicator in the legend', async function (assert) { - await this.createKeyValueInputs({ isFieldsetOptional: true }); + await createKeyValueInputs({ isFieldsetOptional: true }); assert .dom( '#test-form-key-value-inputs .hds-form-key-value-inputs__header legend .hds-form-indicator.hds-form-indicator--optional', @@ -109,7 +115,7 @@ module( // ROW test('it should render the row content if the data argument is an empty array', async function (assert) { - await this.createKeyValueInputs(); + await createKeyValueInputs({}); assert .dom('#test-form-key-value-inputs .hds-form-key-value-inputs__row') .exists({ count: 1 }); @@ -138,7 +144,7 @@ module( }); test('it should render the row content if the data argument has entries', async function (assert) { - await this.createKeyValueInputs({ + await createKeyValueInputs({ data: [{ value: 'Test value' }, { value: 'Another value' }], }); assert @@ -149,7 +155,7 @@ module( '#test-form-key-value-inputs .hds-form-key-value-inputs__row .hds-form-key-value-inputs__field', ) .exists({ count: 2 }); - assert; + assert .dom( '#test-form-key-value-inputs .hds-form-key-value-inputs__generic-container', @@ -163,32 +169,33 @@ module( }); test('it should yield `rowData` and `rowIndex`', async function (assert) { - this.data = [{ key: 'Test key', value: 'Test value' }]; - await render(hbs` - - <:row as |R|> - - This row has R.rowIndex={{R.rowIndex}} / R.rowData.key={{R.rowData.key}} / R.rowData.value={{R.rowData.value}} - - - - `); + const data = [{ key: 'Test key', value: 'Test value' }]; + await render( + , + ); assert .dom( '#test-form-key-value-inputs .hds-form-key-value-inputs__generic-container', ) .hasText( - `This row has R.rowIndex=0 / R.rowData.key=${this.data[0].key} / R.rowData.value=${this.data[0].value}`, + `This row has R.rowIndex=0 / R.rowData.key=${data[0] ? data[0].key : ''} / R.rowData.value=${data[0] ? data[0].value : ''}`, ); }); // FOOTER test('it should render the footer content', async function (assert) { - await this.createKeyValueInputs(); + await createKeyValueInputs({}); assert .dom('#test-form-key-value-inputs .hds-form-key-value-inputs__footer') .exists(); @@ -211,7 +218,7 @@ module( // INLINE STYLES FOR GRID LAYOUT test('it should set the appropriate column indexes for some row children (generic + button) if there is no data', async function (assert) { - await this.createKeyValueInputs(); + await createKeyValueInputs({}); assert .dom( '#test-form-key-value-inputs .hds-form-key-value-inputs__generic-container', @@ -225,38 +232,38 @@ module( }); test('it should set the appropriate column indexes for some row children (generic + button) when there is complex row content and data', async function (assert) { - this.data = [ + const data = [ { key: 'Test key', value: 'Test value' }, { key: 'Another key', value: 'Another value' }, ]; - await render(hbs` - - <:header as |H|> - Legend - - <:row as |R|> - - Key - - - - Generic content - - - Value - - - - {{!-- Adding generic content after the delete row button to ensure it is actually rendered in a column before the delete icon --}} - - Generic content - - - - `); + + await render( + , + ); assert .dom('#test-form-key-value-inputs [data-test-generic-1]') .hasStyle({ '--hds-key-value-inputs-column-index': '2' }); @@ -274,7 +281,7 @@ module( // CUSTOM WIDTHS test('it should set the appropriate `grid-template-columns` CSS property via `--hds-key-value-inputs-columns` for the rows without custom widths', async function (assert) { - await this.createKeyValueInputs({ + await createKeyValueInputs({ data: [{ value: 'Test value' }, { value: 'Another value' }], }); @@ -284,42 +291,41 @@ module( }); test('it should set the appropriate `grid-template-columns` CSS property via `--hds-key-value-inputs-columns` for the rows with complex structure', async function (assert) { - this.data = [ + const data = [ { key: 'Test key', value: 'Test value' }, { key: 'Another key', value: 'Another value' }, ]; - await render(hbs` - - <:row as |R|> - - - - - - - - `); + await render( + , + ); assert.dom('#test-form-key-value-inputs').hasStyle({ '--hds-key-value-inputs-columns': '1fr auto 1fr auto 2.25rem', }); }); test('it should set the appropriate `grid-template-columns` CSS property via `--hds-key-value-inputs-columns` for the row when there is no data and no yielded delete button', async function (assert) { - this.data = []; - await render(hbs` - - <:row as |R|> - - - - - `); + const data: never[] = []; + + await render( + , + ); assert .dom('#test-form-key-value-inputs') // per design specs, the last column is always set to provide space for the delete button, even if the button is not rendered, to avoid layout shifts @@ -327,19 +333,18 @@ module( }); test('it should set the appropriate `grid-template-columns` CSS property via `--hds-key-value-inputs-columns` for the rows with custom widths', async function (assert) { - this.data = [{ value: 'Test value' }, { value: 'Another value' }]; - await render(hbs` - - <:row as |R|> - - - - - - `); + const data = [{ value: 'Test value' }, { value: 'Another value' }]; + await render( + , + ); assert .dom('#test-form-key-value-inputs .hds-form-key-value-inputs__field') @@ -354,24 +359,26 @@ module( test('it should associate together the fieldset and its legend, help text and error', async function (assert) { const extraAriaDescribedBy = 'extra'; - await this.createKeyValueInputs({ extraAriaDescribedBy }); + await createKeyValueInputs({ extraAriaDescribedBy }); - const legendId = document.querySelector( + const legend = find( '#test-form-key-value-inputs .hds-form-key-value-inputs__header legend', - ).id; - const helperId = document.querySelector( + ); + const helper = find( '#test-form-key-value-inputs .hds-form-key-value-inputs__header .hds-form-key-value-inputs__helper-text', - ).id; - const errorId = document.querySelector( + ); + const error = find( '#test-form-key-value-inputs .hds-form-key-value-inputs__error', - ).id; + ); - assert.dom('#test-form-key-value-inputs').hasAria('labelledby', legendId); + assert + .dom('#test-form-key-value-inputs') + .hasAria('labelledby', legend?.id ?? ''); assert .dom('#test-form-key-value-inputs') .hasAria( 'describedby', - `${helperId} ${errorId} ${extraAriaDescribedBy}`, + `${helper?.id} ${error?.id} ${extraAriaDescribedBy}`, ); }); }, diff --git a/showcase/tests/integration/components/hds/form/label/index-test.js b/showcase/tests/integration/components/hds/form/label/index-test.gts similarity index 62% rename from showcase/tests/integration/components/hds/form/label/index-test.js rename to showcase/tests/integration/components/hds/form/label/index-test.gts index acce4ebfd9f..4fc0b0b2863 100644 --- a/showcase/tests/integration/components/hds/form/label/index-test.js +++ b/showcase/tests/integration/components/hds/form/label/index-test.gts @@ -4,20 +4,24 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormLabel } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/label/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-label').hasClass('hds-form-label'); }); test('it should render with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-label').hasClass('my-class'); }); @@ -26,13 +30,18 @@ module('Integration | Component | hds/form/label/index', function (hooks) { test('it renders a label with the defined text', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#test-form-label').hasText('This is the label'); }); test('it renders a label with the yielded content', async function (assert) { await render( - hbs`
This is an HTML element inside the label
`, + , ); assert.dom('#test-form-label > pre').exists(); assert @@ -42,7 +51,10 @@ module('Integration | Component | hds/form/label/index', function (hooks) { test('it renders hidden text if set @hiddenText', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#test-form-label').hasText('This is the label this is hidden'); assert.dom('#test-form-label .sr-only').hasText('this is hidden'); @@ -52,14 +64,20 @@ module('Integration | Component | hds/form/label/index', function (hooks) { test('it appends an indicator to the label text when user input is required', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#test-form-label .hds-form-indicator').exists(); assert.dom('#test-form-label .hds-form-indicator').hasText('Required'); }); test('it appends an indicator to the label text when user input is optional', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#test-form-label > .hds-form-indicator').exists(); assert.dom('#test-form-label .hds-form-indicator').hasText('(Optional)'); @@ -69,16 +87,28 @@ module('Integration | Component | hds/form/label/index', function (hooks) { test('it renders a label with the "for" attribute if the @controlId argument is provided', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#test-form-label').hasAttribute('for', 'my-control-id'); }); test('it renders a label with the "for" attribute even if the @controlId argument is a boolean', async function (assert) { await render( - hbs`True - False - `, + , ); assert.dom('#test-form-label-true').hasAttribute('for', 'true'); assert.dom('#test-form-label-false').hasAttribute('for', 'false'); @@ -88,7 +118,9 @@ module('Integration | Component | hds/form/label/index', function (hooks) { test('it renders a label with the correct "id" attribute if the @controlId argument is provided', async function (assert) { await render( - hbs`This is the label`, + , ); assert.dom('#label-my-control-id').exists(); }); diff --git a/showcase/tests/integration/components/hds/form/legend/index-test.js b/showcase/tests/integration/components/hds/form/legend/index-test.gts similarity index 67% rename from showcase/tests/integration/components/hds/form/legend/index-test.js rename to showcase/tests/integration/components/hds/form/legend/index-test.gts index d3c8a6ec5c7..89e18d37c26 100644 --- a/showcase/tests/integration/components/hds/form/legend/index-test.js +++ b/showcase/tests/integration/components/hds/form/legend/index-test.gts @@ -4,30 +4,34 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormLegend } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module('Integration | Component | hds/form/legend/index', function (hooks) { setupRenderingTest(hooks); test('it should render the component with a CSS class that matches the component name', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-legend').hasClass('hds-form-legend'); }); test('it renders the element as ', async function (assert) { - await render(hbs``); + await render(); assert.dom('#test-form-legend').hasTagName('legend'); }); test('it should render with a CSS class provided via the @contextualClass argument', async function (assert) { await render( - hbs``, + , ); assert.dom('#test-form-legend').hasClass('my-class'); }); test('it should set the id correctly if pass @id argument', async function (assert) { - await render(hbs``); + await render(); assert.dom('#custom-id').exists(); }); @@ -35,13 +39,18 @@ module('Integration | Component | hds/form/legend/index', function (hooks) { test('it renders a legend with the defined text', async function (assert) { await render( - hbs`This is the legend`, + , ); assert.dom('#test-form-legend').hasText('This is the legend'); }); test('it renders a legend with the yielded content', async function (assert) { await render( - hbs`
This is an HTML element inside the legend
`, + , ); assert.dom('#test-form-legend > pre').exists(); assert @@ -53,14 +62,20 @@ module('Integration | Component | hds/form/legend/index', function (hooks) { test('it appends an indicator to the legend text when user input is required', async function (assert) { await render( - hbs`This is the legend`, + , ); assert.dom('#test-form-legend .hds-form-indicator').exists(); assert.dom('#test-form-legend .hds-form-indicator').hasText('Required'); }); test('it appends an indicator to the legend text when user input is optional', async function (assert) { await render( - hbs`This is the legend`, + , ); assert.dom('#test-form-legend > .hds-form-indicator').exists(); assert.dom('#test-form-legend .hds-form-indicator').hasText('(Optional)'); diff --git a/showcase/tests/integration/components/hds/form/masked-input/base-test.js b/showcase/tests/integration/components/hds/form/masked-input/base-test.gts similarity index 72% rename from showcase/tests/integration/components/hds/form/masked-input/base-test.js rename to showcase/tests/integration/components/hds/form/masked-input/base-test.gts index af866cfe9fc..fe939cc8c73 100644 --- a/showcase/tests/integration/components/hds/form/masked-input/base-test.js +++ b/showcase/tests/integration/components/hds/form/masked-input/base-test.gts @@ -4,9 +4,11 @@ */ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; + +import { HdsFormMaskedInputBase } from '@hashicorp/design-system-components/components'; + +import { setupRenderingTest } from 'showcase/tests/helpers'; module( 'Integration | Component | hds/form/masked-input/base', @@ -15,7 +17,9 @@ module( test('it should render the component with a specific CSS class', async function (assert) { await render( - hbs``, + , ); assert .dom('#test-form-masked-input') @@ -24,7 +28,12 @@ module( test('it should set aria-describedby and id arguments if pass @id or @ariaDescribedBy', async function (assert) { await render( - hbs``, + , ); assert .dom('#custom-id') @@ -36,7 +45,9 @@ module( test('it should render the text masked by default', async function (assert) { await render( - hbs``, + , ); assert .dom('.hds-form-masked-input__control') @@ -46,7 +57,12 @@ module( test('it should render readable text when `isContentMasked` is false', async function (assert) { await render( - hbs``, + , ); assert .dom('.hds-form-masked-input__control') @@ -56,7 +72,9 @@ module( test('it should toggle the masking when button is pressed', async function (assert) { await render( - hbs``, + , ); await click('.hds-form-visibility-toggle'); assert @@ -69,7 +87,9 @@ module( test('it automatically provides the ID relations between the elements', async function (assert) { await render( - hbs``, + , ); assert .dom('.hds-form-visibility-toggle') @@ -78,7 +98,9 @@ module( test('it updates the button label on toggle', async function (assert) { await render( - hbs``, + , ); assert .dom('.hds-form-visibility-toggle') @@ -91,7 +113,9 @@ module( test('it informs the user about visibility change on toggle', async function (assert) { await render( - hbs``, + , ); await click('.hds-form-visibility-toggle'); assert @@ -107,14 +131,21 @@ module( test('it should render an `` element by default', async function (assert) { await render( - hbs``, + , ); assert.dom('input#test-form-masked-input').exists(); }); test('it should render a ``); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual(this.value, getTextToCopyFromTargetElement(this.target)); }); @@ -128,11 +148,15 @@ module( this.set('option1', `option1`); this.set('option2', `option2`); this.set('option3', `option3`); - await render(hbs``); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual( this.option1, @@ -144,11 +168,15 @@ module( this.set('option1', `option1`); this.set('option2', `option2`); this.set('option3', `option3`); - await render(hbs``); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual( this.option2, @@ -162,10 +190,16 @@ module( }); test('returns the innerText of DOM element passed as `target` argument', async function (assert) { - await render(hbs`
    -
  • Lorem Ipsum dolor

  • -
  • Sit Amet

    Some
    Code
  • -
`); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual( getTextToCopyFromTargetElement(this.target), @@ -174,19 +208,27 @@ module( }); test('returns the innerText of DOM element passed as `target` argument without including hidden elements', async function (assert) { - await render(hbs`

Lorem - Ipsum - Dolor -

`); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual(getTextToCopyFromTargetElement(this.target), 'Lorem '); }); test('returns the innerText of DOM element passed as `target` argument without including sr only text', async function (assert) { - await render(hbs`

- Lorem ipsum - Text not to copy -

`); + await render( + , + ); this.target = find('#test-target'); assert.deepEqual( getTextToCopyFromTargetElement(this.target), @@ -264,11 +306,16 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { test('it should allow to copy a `string` provided as `@text` argument', async function (assert) { await render( - hbs``, + , ); await click('button#test-button'); assert.true(this.success); @@ -276,11 +323,12 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { test('it should copy an empty string provided as a `@text` argument', async function (assert) { await render( - hbs``, + , ); await click('button#test-button'); assert.true(this.success); @@ -289,11 +337,16 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { // context: https://github.com/hashicorp/design-system/pull/1564 test('it should allow to copy an `integer` provided as `@text` argument', async function (assert) { await render( - hbs``, + , ); await click('button#test-button'); assert.true(this.success); @@ -301,11 +354,12 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { test('it should copy a zero number value provided as a `@text` argument', async function (assert) { await render( - hbs``, + , ); await click('button#test-button'); assert.true(this.success); @@ -315,22 +369,34 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { test('it should allow to target an element using a `string` selector for the `@target` argument', async function (assert) { await render( - hbs`

Hello world!

`, + , ); await click('button#test-button'); assert.true(this.success); }); test('it should allow to target an element using a DOM node', async function (assert) { - await render(hbs`

Hello world!

`); + await render( + , + ); this.set('target', find('#test-target')); await click('button#test-button'); assert.true(this.success); @@ -340,11 +406,16 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { test('it should invoke the `onSuccess` callback on a successful "copy" action', async function (assert) { await render( - hbs``, + , ); await click('button#test-button'); assert.true(this.success); @@ -359,11 +430,16 @@ module('Integration | Modifier | hds-clipboard', function (hooks) { 'this is a fake error message provided to the sinon.stub().throws() method', ); await render( - hbs``, + , ); await click('button#test-button'); assert.false(this.success); diff --git a/showcase/tests/integration/modifiers/hds-register-event-test.js b/showcase/tests/integration/modifiers/hds-register-event-test.gjs similarity index 69% rename from showcase/tests/integration/modifiers/hds-register-event-test.js rename to showcase/tests/integration/modifiers/hds-register-event-test.gjs index cd45dded0d4..f02d76359ec 100644 --- a/showcase/tests/integration/modifiers/hds-register-event-test.js +++ b/showcase/tests/integration/modifiers/hds-register-event-test.gjs @@ -6,7 +6,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'showcase/tests/helpers'; import { click, render, triggerEvent } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; +import hdsRegisterEvent from '@hashicorp/design-system-components/modifiers/hds-register-event'; module('Integration | Modifier | hds-register-event', function (hooks) { setupRenderingTest(hooks); @@ -19,7 +19,12 @@ module('Integration | Modifier | hds-register-event', function (hooks) { }); await render( - hbs``, + , ); await click('button'); @@ -37,7 +42,12 @@ module('Integration | Modifier | hds-register-event', function (hooks) { }); await render( - hbs``, + , ); await triggerEvent('span', 'click', { bubbles: true });