// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// /* eslint-disable no-undef */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ZuiOptionElement, ZuiSelectDropdownElement } from '@zywave/zui-select'; import { ZuiDialogElement } from '@zywave/zui-dialog'; import { assert } from '@esm-bundle/chai'; import { awaitEvent, randNumber, randString, sleep } from '../../../../test/src/util/helpers.js'; import { executeServerCommand, sendKeys } from '@web/test-runner-commands'; async function open(element: ZuiSelectDropdownElement) { await element.updateComplete; element.showPicker(); await sleep(10); await element.updateComplete; } async function close(element: ZuiSelectDropdownElement) { element.closest('body')?.click(); await sleep(10); await element.updateComplete; } async function selectOption(element: ZuiSelectDropdownElement, index: number) { await open(element); const option = element.item(index); assert.exists(option, `No option found with index ${index}.`); option.selected = true; await close(element); return option.value; } async function toggleSelectAllOption(element: ZuiSelectDropdownElement) { // forgive me, as I hack around the inability to simply "click" on DOM element await open(element); (element as any)._highlightedIndex = -1; element.requestUpdate(); await element.updateComplete; (element.shadowRoot?.querySelector('input') as HTMLElement).dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter' }) ); await close(element); } function generateOptions(element: ZuiSelectDropdownElement, optionCount: number) { const result: ZuiOptionElement[] = []; for (let i = 0; i < optionCount; i++) { const option: ZuiOptionElement = document.createElement('zui-option'); option.value = randString(); option.innerText = randString(); element.append(option); result.push(option); } return result; } suite('zui-select-dropdown', () => { let element: ZuiSelectDropdownElement; setup(() => { element = document.createElement('zui-select-dropdown'); document.body.append(element); }); teardown(() => { element.remove(); }); test('initializes as a ZuiSelectDropdownElement', () => { assert.instanceOf(element, ZuiSelectDropdownElement); }); test('item method exposes matching option', async () => { const options = generateOptions(element, 10); await open(element); for (let i = 0; i < 10; i++) { const option = element.item(i); assert.exists(option); assert.equal(option.value, options[i].value); assert.equal(option.label, options[i].innerHTML); } }); test('item method returns null when no options', async () => { await open(element); const option = element.item(0); assert.isNull(option); }); test('item method returns null when index out of bounds', async () => { generateOptions(element, 10); await open(element); const option = element.item(11); assert.isNull(option); }); test('queryHandler is called with user input', async () => { element.searchable = true; let resultQuery: string | undefined; element.queryHandler = (query, options) => { resultQuery = query; return options; }; await element.updateComplete; const text = randString(); await executeServerCommand('fill-text', { selector: `zui-select-dropdown input`, text }); await element.updateComplete; assert.equal(resultQuery, text); }); test('user input triggers query event', async () => { element.searchable = true; element.debounce = 1_000; await element.updateComplete; const text = randString(); const userInputPromise = executeServerCommand('fill-text', { selector: `zui-select-dropdown input`, text }).then( () => element.updateComplete ); let queryEvent: CustomEvent; const queryPromise = awaitEvent(element, 'query').then((e) => (queryEvent = e as CustomEvent)); await Promise.all([userInputPromise, queryPromise]); assert.exists(queryEvent); assert.equal(queryEvent.detail, text); }); test('empty groups are not rendered with hide-empty-groups attribute', async () => { element.hideEmptyGroups = true; const group1 = document.createElement('zui-option-group'); group1.label = 'Group 1'; const option1 = document.createElement('zui-option'); option1.value = 'option1'; option1.innerText = 'Test Option 1'; group1.append(option1); element.append(group1); const group2 = document.createElement('zui-option-group'); group2.label = 'Group 2'; element.append(group2); element.requestUpdate(); await element.updateComplete; await open(element); const renderedOptionGroups = element.shadowRoot?.querySelectorAll('.group-label'); assert.exists(renderedOptionGroups); assert.lengthOf(renderedOptionGroups, 1); assert.equal(renderedOptionGroups?.[0].textContent?.trim(), 'Group 1'); }); suite('single select', () => { const optionCount = 10; setup(() => { element.multiple = false; generateOptions(element, optionCount); }); suite('searchable behavior', () => { setup(() => { element.searchable = true; }); // disabled test which violates encapsulation test.skip('modifying input without clearing it does not modify selected property of option', async () => { // // Choose an option that is NOT the first // const optionIndex = 1 + randNumber(optionCount - 1); // const option = element._zuiOptions[optionIndex]; // assert.exists(option, `No option found at index ${optionIndex}`); // option.selected = true; // element.requestUpdate(); // await element.updateComplete; // const text = randString(); // element.shadowRoot?.querySelector('input')?.focus(); // sendKeys({ // type: text, // }); // await awaitEvent(element.shadowRoot?.querySelector('input') as HTMLInputElement, 'input', 1000); // await element.updateComplete; // assert.isTrue(element._zuiOptions[optionIndex].selected); }); test('modifying input value which can be parsed as falsy does not modify selected property of option', async () => { const option: ZuiOptionElement = document.createElement('zui-option'); const almostFalsyValue = 'fals'; option.value = almostFalsyValue; option.innerText = almostFalsyValue; option.selected = true; element.append(option); element.requestUpdate(); await element.updateComplete; element.shadowRoot?.querySelector('input')?.focus(); sendKeys({ type: 'e', }); await awaitEvent(element.shadowRoot?.querySelector('input') as HTMLInputElement, 'input'); await element.updateComplete; assert.isTrue(option.selected); }); }); suite('form association', () => { let form: HTMLFormElement; const name = randString(); setup(() => { form = document.createElement('form'); document.body.append(form); form.append(element); element.setAttribute('name', name); }); teardown(() => { form.remove(); }); test('selected option value included in form submission', async () => { const value = await selectOption(element, randNumber(optionCount)); const formData = new FormData(form); const formValues = formData.getAll(name); assert.isNotEmpty(formValues); assert.equal(formValues.length, 1); assert.include(formValues, value); }); test('only one selected option value included in form submission', async () => { let value; for (let i = 0; i < 3; i++) { value = await selectOption(element, i); } const formData = new FormData(form); const formValues = formData.getAll(name); assert.isNotEmpty(formValues); assert.equal(formValues.length, 1); assert.include(formValues, value); }); }); }); suite('multiselect', () => { const optionCount = 10; setup(() => { element.multiple = true; generateOptions(element, optionCount); }); suite('truncation', () => { test('maximumResultsDisplayCount defaults to 5', () => { assert.equal(element.maximumResultsDisplayCount, 5); }); test('truncatedResultMessageFormat defaults to null', () => { assert.isNull(element.truncatedResultMessageFormat); }); test('truncation does not occur when truncatedResultMessageFormat is null', async () => { element.truncatedResultMessageFormat = null; let selection = 6; while (selection > 0) { await selectOption(element, --selection); } assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, 6); assert.notExists(element.shadowRoot?.querySelector('.selection.truncated')); }); test('truncation is applied when more than maximumResultsDisplayCount selected', async () => { element.truncatedResultMessageFormat = '{0}'; element.maximumResultsDisplayCount = 2; let selection = 3; while (selection > 0) { await selectOption(element, --selection); } assert.equal(element.shadowRoot?.querySelectorAll('.selection:not(.truncated)').length, 2); assert.exists(element.shadowRoot?.querySelector('.selection.truncated')); }); }); suite('enable-select-all validation', () => { test('enable-select-all is false when multiple is not set', () => { element.multiple = false; element.selectAllOptionLabel = 'Select all'; element.enableSelectAll = true; element.enableSelectAllOverride = false; assert.isFalse(element.enableSelectAll); }); test('enable-select-all is false when selectAllOptionLabel is not set', () => { element.multiple = false; element.selectAllOptionLabel = 'Select all'; element.enableSelectAll = true; element.enableSelectAllOverride = false; assert.isFalse(element.enableSelectAll); }); suite('enable-select-all-override is true', () => { setup(() => { element.enableSelectAllOverride = true; element.selectAllOptionLabel = 'Select all'; element.enableSelectAll = true; element.selectAllOptionValue = ''; element.selectAllResultLabel = 'All selected'; }); test('enable-select-all is false when selectAllOptionValue is not set', () => { element.selectAllOptionValue = null; assert.isFalse(element.enableSelectAll); }); test('enable-select-all is false when selectAllResultLabel is not set', () => { element.selectAllResultLabel = null; assert.isFalse(element.enableSelectAll); }); test('enable-select-all is true', () => { assert.isTrue(element.enableSelectAll); }); }); suite('enable-select-all-override is false', () => { setup(() => { element.enableSelectAllOverride = false; element.selectAllOptionLabel = 'Select all'; element.enableSelectAll = true; }); test('enable-select-all is true', () => { assert.isTrue(element.enableSelectAll); }); }); }); suite('select all behavior', () => { setup(() => { element.enableSelectAllOverride = false; element.selectAllOptionLabel = 'Select all'; element.enableSelectAll = true; }); suite('enable-select-all-override is false', () => { test('when user selects all other options manually, select all option is checked', async () => { let selection = optionCount; while (selection > 0) { await selectOption(element, --selection); } assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount); assert.isTrue(element.allSelected); await open(element); assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected')); }); test('when user chooses select all option, all options selected in result container', async () => { await toggleSelectAllOption(element); assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount); assert.isTrue(element.allSelected); await open(element); assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected')); }); }); suite('enable-select-all-override is true', () => { setup(() => { element.enableSelectAllOverride = true; element.selectAllOptionValue = ''; element.selectAllResultLabel = 'All selected'; }); test('when user selects all other options manually, selectAllResultLabel is not rendered instead', async () => { let selection = optionCount; while (selection > 0) { await selectOption(element, --selection); } assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, optionCount); assert.notExists(element.shadowRoot?.querySelector('.selection.all-selected')); assert.isFalse(element.allSelected); }); test('when user chooses select all option, selectAllResultLabel is rendered instead', async () => { await toggleSelectAllOption(element); assert.equal(element.shadowRoot?.querySelectorAll('.selection').length, 1); assert.exists(element.shadowRoot?.querySelector('.selection.all-selected')); assert.isTrue(element.allSelected); }); test('when user chooses select all option, selectAllOptionValue is only value in form submission', async () => { const form = document.createElement('form'); element.setAttribute('name', 'test'); form.append(element); await toggleSelectAllOption(element); const formData = new FormData(form); const values = formData.getAll('test'); assert.equal(values.length, 1); assert.include(values, element.selectAllOptionValue); form.remove(); }); }); }); suite('form association', () => { let form: HTMLFormElement; const name = randString(); setup(() => { form = document.createElement('form'); document.body.append(form); form.append(element); element.setAttribute('name', name); }); teardown(() => { form.remove(); }); test('multiple selected option values included in form submission', async () => { const values: string[] = []; for (let i = 0; i < 3; i++) { const value = await selectOption(element, i); values.push(value); } const formData = new FormData(form); const formValues = formData.getAll(name); assert.isNotEmpty(formValues); assert.equal(formValues.length, values.length); assert.includeMembers(formValues, values); }); }); test('clear() properly clears all selected options', async () => { const values: string[] = []; for (let i = 0; i < 3; i++) { const value = await selectOption(element, i); values.push(value); } assert.equal((element as any)._formValue?.length, values.length); element.clear(); await element.updateComplete; assert.isNull((element as any)._formValue); }); }); suite('zui-select-dropdown in dialogs', () => { let dialog: ZuiDialogElement; setup(() => { dialog = document.createElement('zui-dialog') as ZuiDialogElement; document.body.append(dialog); }); teardown(() => { dialog.remove(); }); test('zui-select-dropdown is in a scrolling dialog', async () => { const longContentDiv = document.createElement('div'); longContentDiv.setAttribute('slot', 'content'); longContentDiv.style.height = '10000px'; longContentDiv.append(element); dialog.append(longContentDiv); dialog.open(); await dialog.updateComplete; assert.isTrue(dialog.opened, 'zui-dialog is opened'); const dialogIsScrolling = dialog.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling'); assert.exists(dialogIsScrolling, 'zui-dialog is scrolling'); assert.exists(dialog.querySelector('zui-select-dropdown'), 'zui-select-dropdown is, in fact, in zui-dialog'); await open(element); assert.exists( element.shadowRoot?.querySelector('.in-scrolling-dialog'), 'zui-select-dropdown is in a dialog that scrolls, therefore the wrapper has the class "in-scrolling-dialog"' ); }); test('zui-select-dropdown is in a dialog that is not scrolling', async () => { const dialogIsScrolling = dialog.shadowRoot?.querySelector('dialog')?.classList.contains('scrolling'); dialog.append(element); dialog.open(); await open(element); assert.isTrue(dialog.opened, 'zui-dialog is opened'); assert.notExists(dialogIsScrolling, 'zui-dialog is not scrolling'); assert.exists(dialog.querySelector('zui-select-dropdown'), 'zui-select-dropdown is, in fact, in zui-dialog'); assert.notExists( dialog.shadowRoot?.querySelector('.in-scrolling-dialog'), 'zui-select-dropdown is in a dialog that does not scroll' ); }); }); suite('initial state detection', () => { let element: ZuiSelectDropdownElement; const optionCount = 10; setup(() => { element = document.createElement('zui-select-dropdown'); element.multiple = true; element.enableSelectAll = true; element.selectAllOptionLabel = 'Select all'; document.body.append(element); }); teardown(() => { element.remove(); }); test('correctly detects when all options are initially selected', async () => { // Generate options and set them all as selected const options = generateOptions(element, optionCount); options.forEach((option) => { option.selected = true; }); // Wait for the element to initialize await element.updateComplete; // Verify the initial state assert.isTrue(element.allSelected); await open(element); assert.exists(element.shadowRoot?.querySelector('.option.select-all.selected')); await toggleSelectAllOption(element); assert.isFalse(element.allSelected); // check if each option is selected options.forEach((option) => { assert.isFalse(option.selected); }); }); }); suite('async loading with queryHandler', () => { let element: ZuiSelectDropdownElement; let resolveQuery: (value: any) => void; setup(() => { element = document.createElement('zui-select-dropdown'); element.searchable = true; document.body.append(element); }); teardown(() => { element.remove(); }); test('does not show "no results found" message while async loading', async () => { // Set up a queryHandler that returns a promise we can control element.queryHandler = () => { return new Promise((resolve) => { resolveQuery = resolve; }); }; await element.updateComplete; // Open the dropdown to trigger the queryHandler await open(element); // At this point, the queryHandler has been called but not resolved // The "no results found" message should NOT be visible const noResultsMessage = element.shadowRoot?.querySelector('.option.readonly'); assert.notExists(noResultsMessage, 'No results message should not be shown while loading'); // Verify the spinner is shown instead const spinner = element.shadowRoot?.querySelector('zui-spinner[active]'); assert.exists(spinner, 'Spinner should be shown while loading'); // Now resolve the query with no results resolveQuery([]); await element.updateComplete; await sleep(10); // After async loading completes with no results, the message SHOULD be shown const noResultsMessageAfter = element.shadowRoot?.querySelector('.option.readonly'); assert.exists( noResultsMessageAfter, 'No results message should be shown after loading completes with no results' ); assert.equal(noResultsMessageAfter?.textContent, 'No results'); }); test('shows options after async loading completes with results', async () => { // Set up a queryHandler that returns a promise we can control element.queryHandler = () => { return new Promise((resolve) => { resolveQuery = resolve; }); }; await element.updateComplete; // Open the dropdown to trigger the queryHandler await open(element); // The "no results found" message should NOT be visible while loading const noResultsMessageWhileLoading = element.shadowRoot?.querySelector('.option.readonly'); assert.notExists(noResultsMessageWhileLoading, 'No results message should not be shown while loading'); // Resolve the query with some results resolveQuery([ { label: 'Option 1', value: 'opt1' }, { label: 'Option 2', value: 'opt2' }, ]); await element.updateComplete; await sleep(10); // After async loading completes with results, options should be shown const options = element.shadowRoot?.querySelectorAll('.option:not(.readonly)'); assert.exists(options); assert.isAtLeast(options.length, 2, 'Options should be shown after loading completes'); // No results message should NOT be shown const noResultsMessageAfter = element.shadowRoot?.querySelector('.option.readonly'); assert.notExists(noResultsMessageAfter, 'No results message should not be shown when options are available'); }); }); });