import { type E2EElement, type E2EPage, newE2EPage } from '@stencil/core/testing'; import { CREATE_NEW_ID } from '../../src/controller/ods-combobox'; describe('ods-combobox rendering', () => { let el: E2EElement; let innerOdsInput: HTMLInputElement; let input: E2EElement; let page: E2EPage; async function isOpen(): Promise { return await page.evaluate(() => { return !!document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__results--open'); }); } async function setup(content: string, customStyle?: string): Promise { page = await newE2EPage(); await page.setContent(content); await page.evaluate(() => document.body.style.setProperty('margin', '0')); if (customStyle) { await page.addStyleTag({ content: customStyle }); } el = await page.find('ods-combobox'); input = await page.find('ods-combobox >>> ods-input'); innerOdsInput = el.shadowRoot!.querySelector('ods-input')!; } it('should render the web component', async() => { await setup(''); expect(el.shadowRoot).not.toBeNull(); }); it('should render a placeholder', async() => { const dummyPlaceholder = 'dummy placeholder'; await setup(``); expect(innerOdsInput.getAttribute('placeholder')).toBe(dummyPlaceholder); }); it('should render disabled', async() => { await setup(''); expect(innerOdsInput.hasAttribute('is-disabled')).toBe(true); }); it('should render readonly', async() => { await setup(''); expect(innerOdsInput.hasAttribute('is-readonly')).toBe(true); }); describe('error state', () => { async function hasErrorClass(): Promise { return await page.evaluate(() => { return !!document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__search--error'); }); } it('should render in error on form submit, before any changes, if invalid', async() => { await setup('
'); await page.evaluate(() => { document.querySelector('form')?.requestSubmit(); }); await page.waitForChanges(); expect(await hasErrorClass()).toBe(true); }); it('should toggle the error state on value change', async() => { await setup('
Value
'); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); await (await page.find('ods-combobox-item')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(false); expect(await hasErrorClass()).toBe(false); }); it('should enforce the error state if has-error is set even on valid combobox', async() => { await setup('
'); await page.waitForChanges(); expect(await hasErrorClass()).toBe(true); await page.evaluate(() => { document.querySelector('form')?.requestSubmit(); }); await page.waitForChanges(); expect(await hasErrorClass()).toBe(true); }); }); describe('watchers', () => { describe('isDisabled', () => { it('should disable the combobox component and close the dropdown', async() => { await setup('Dummy'); await el.callMethod('open'); await page.waitForChanges(); expect(innerOdsInput.hasAttribute('is-disabled')).toBe(false); expect(await isOpen()).toBe(true); el.setAttribute('is-disabled', true); await page.waitForChanges(); expect(await page.evaluate(() => { return document.querySelector('ods-combobox')?.shadowRoot?.querySelector('ods-input')?.hasAttribute('is-disabled'); })).toBe(true); expect(await isOpen()).toBe(false); }); }); describe('new element', () => { async function hasAddElementItem(): Promise { return page.evaluate((addItemId: string) => { return !!document.querySelector('ods-combobox')?.shadowRoot?.querySelector(`#${addItemId}`); }, CREATE_NEW_ID); } it('should offer to add a new item', async() => { await setup('Dummy'); await input.type('C', { delay: 200 }); await page.waitForChanges(); expect(await hasAddElementItem()).toBe(true); }); it('should not offer to add a new item', async() => { await setup('Dummy'); await input.type('C', { delay: 200 }); await page.waitForChanges(); expect(await hasAddElementItem()).toBe(false); }); }); describe('highlightResults', () => { async function getHighlightedContent(): Promise { return page.evaluate(() => { return document.querySelector('ods-combobox-item > .ods-combobox-item--highlighted')?.textContent; }); } it('should add/remove marker around the matching part of the item', async() => { await setup('Dummy'); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await getHighlightedContent()).toBe(undefined); await input.type('D', { delay: 200 }); await page.waitForChanges(); expect(await getHighlightedContent()).toBe('D'); el.removeAttribute('highlight-results'); await page.waitForChanges(); expect(await getHighlightedContent()).toBe(undefined); }); }); describe('onSlotChange', () => { it('should change items but not the selected one', async() => { const dummyValue1 = 'dummy value 1'; const dummyValue2 = 'dummy value 2'; await setup(`Dummy 1`); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect((await page.find('ods-combobox-item')).textContent).toBe('Dummy 1'); await (await page.find('ods-combobox-item')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(false); expect(await el.getProperty('value')).toBe(dummyValue1); await page.evaluate((value) => { document.querySelector('ods-combobox')!.innerHTML = `Dummy 2`; }, dummyValue2); await page.waitForChanges(); expect(await el.getProperty('value')).toBe(dummyValue1); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect((await page.find('ods-combobox-item')).textContent).toBe('Dummy 2'); await (await page.find('ods-combobox-item')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(false); expect(await el.getProperty('value')).toBe(`${dummyValue1},${dummyValue2}`); }); it('should keep correct group visibility when adding/removing groups', async() => { async function isGroupVisible(id: string): Promise { return page.evaluate((elementId) => { const groupTitle = document.querySelector(`#${elementId}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return !!groupTitle && !groupTitle.classList.contains('ods-combobox-group__title--hidden'); }, id); } await setup(` Group 1 Dummy 1 Group 2 Dummy 2 `); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(true); await page.evaluate(() => { document.querySelector('ods-combobox')!.innerHTML = ` Group 1 Dummy 1 `; }); await page.waitForChanges(); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(false); await page.evaluate(() => { document.querySelector('ods-combobox')!.innerHTML = ` Group 1 Dummy 1 Group 2 Dummy 2 `; }); await page.waitForChanges(); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(true); }); }); describe('onGroupsSlotChange', () => { it('should correctly update groups when adding a group asynchronously', async() => { async function isGroupVisible(id: string): Promise { return page.evaluate((elementId) => { const groupTitle = document.querySelector(`#${elementId}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return !!groupTitle && !groupTitle.classList.contains('ods-combobox-group__title--hidden'); }, id); } await setup(` Group 1 Dummy 1 `); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await isGroupVisible('group1')).toBe(true); await page.evaluate(() => { document.querySelector('ods-combobox')!.innerHTML = ` Group 1 Dummy 1 Group 2 Dummy 2 `; }); // We need to wait until the component remounted the list await page.waitForFunction((group1Id, group2Id) => { const group1Title = document.querySelector(`#${group1Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); const group2Title = document.querySelector(`#${group2Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return ( !!group1Title && !group1Title.classList.contains('ods-combobox-group__title--hidden') && !!group2Title && !group2Title.classList.contains('ods-combobox-group__title--hidden') ); }, {}, 'group1', 'group2'); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(true); }); }); it('should correctly update groups when removing a group asynchronously', async() => { async function isGroupVisible(id: string): Promise { return page.evaluate((elementId) => { const groupTitle = document.querySelector(`#${elementId}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return !!groupTitle && !groupTitle.classList.contains('ods-combobox-group__title--hidden'); }, id); } await setup(` Group 1 Dummy 1 Group 2 Dummy 2 `); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(true); await page.evaluate(() => { document.querySelector('ods-combobox')!.innerHTML = ` Group 1 Dummy 1 `; }); await page.waitForFunction((group1Id, group2Id) => { const group1Title = document.querySelector(`#${group1Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); const group2Element = document.querySelector(`#${group2Id}`); return ( !!group1Title && !group1Title.classList.contains('ods-combobox-group__title--hidden') && !group2Element ); }, {}, 'group1', 'group2'); expect(await isOpen()).toBe(true); expect(await isGroupVisible('group1')).toBe(true); const group2Exists = await page.evaluate(() => { return !!document.querySelector('#group2'); }); expect(group2Exists).toBe(false); }); it('should correctly render groups when loading multiple groups asynchronously', async() => { async function isGroupVisible(id: string): Promise { return page.evaluate((elementId) => { const groupTitle = document.querySelector(`#${elementId}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return !!groupTitle && !groupTitle.classList.contains('ods-combobox-group__title--hidden'); }, id); } await setup(''); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); await page.evaluate(() => { document.querySelector('ods-combobox')!.innerHTML = ` Group 1 Dummy 1 Group 2 Dummy 2 Group 3 Dummy 3 `; }); await page.waitForFunction((group1Id, group2Id, group3Id) => { const group1Title = document.querySelector(`#${group1Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); const group2Title = document.querySelector(`#${group2Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); const group3Title = document.querySelector(`#${group3Id}`)?.shadowRoot?.querySelector('.ods-combobox-group__title'); return ( !!group1Title && !group1Title.classList.contains('ods-combobox-group__title--hidden') && !!group2Title && !group2Title.classList.contains('ods-combobox-group__title--hidden') && !!group3Title && !group3Title.classList.contains('ods-combobox-group__title--hidden') ); }, {}, 'group1', 'group2', 'group3'); expect(await isOpen()).toBe(true); expect(await isGroupVisible('group1')).toBe(true); expect(await isGroupVisible('group2')).toBe(true); expect(await isGroupVisible('group3')).toBe(true); }); }); describe('dropdown positioning', () => { it('should position the dropdown correctly on first render', async() => { await setup(`
Item 1 Item 2 Item 3 Item 4 Item 5
`); const inputEl = await page.find('ods-combobox >>> ods-input'); await inputEl.click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); const isPositionedTop = await page.evaluate(() => { const results = document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__results'); const searchEl = document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__search'); return results?.classList.contains('ods-combobox__results--top') && searchEl?.classList.contains('ods-combobox__search--top'); }); expect(isPositionedTop).toBe(true); }); it('should maintain correct positioning when reopening the dropdown', async() => { await setup(`
Item 1 Item 2 Item 3 Item 4 Item 5
`); const inputEl = await page.find('ods-combobox >>> ods-input'); await inputEl.click(); await page.waitForChanges(); let isPositionedTop = await page.evaluate(() => { const results = document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__results'); return results?.classList.contains('ods-combobox__results--top'); }); expect(isPositionedTop).toBe(true); await page.keyboard.press('Escape'); await page.waitForChanges(); await inputEl.click(); await page.waitForChanges(); isPositionedTop = await page.evaluate(() => { const results = document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__results'); return results?.classList.contains('ods-combobox__results--top'); }); expect(isPositionedTop).toBe(true); }); }); describe('in a fixed context (like ods-modal)', () => { async function setupModal(content: string): Promise { page = await newE2EPage(); await page.setContent(` ${content} `); await page.evaluate(() => { const dialog = document.querySelector('ods-modal')?.shadowRoot?.querySelector('dialog'); dialog?.style.setProperty('animation', 'none'); }); el = await page.find('ods-combobox'); await el.click(); await page.waitForChanges(); } it('should position regarding dialog element (thus not at the right place) in absolute strategy', async() => { await setupModal('Dummy 1Dummy 2'); const hasScroll = await page.evaluate(() => { const dialogContent = document.querySelector('ods-modal')?.shadowRoot?.querySelector('.ods-modal__dialog__content'); return dialogContent && dialogContent.scrollHeight > dialogContent.clientHeight; }); expect(hasScroll).toBe(true); }); it('should position regarding viewport (thus at the right place) in fixed strategy', async() => { await setupModal('Dummy 1Dummy 2'); const hasScroll = await page.evaluate(() => { const dialogContent = document.querySelector('ods-modal')?.shadowRoot?.querySelector('.ods-modal__dialog__content'); return dialogContent && dialogContent.scrollHeight > dialogContent.clientHeight; }); expect(hasScroll).toBe(false); }); }); describe('part', () => { it('should render with custom style applied', async() => { const customHeight = '2000px'; await setup( 'Dummy 1Dummy 2', `ods-combobox::part(list) { max-height: ${customHeight}; }`, ); await (await page.find('ods-combobox >>> ods-input')).click(); await page.waitForChanges(); expect(await isOpen()).toBe(true); const listStyle = await (await page.find('ods-combobox >>> ul')).getComputedStyle(); expect(listStyle.getPropertyValue('max-height')).toBe(customHeight); }); }); describe('empty state', () => { async function hasEmptyResultsClass(): Promise { return await page.evaluate(() => { return !!document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__results--empty'); }); } async function hasEmptySearchClass(): Promise { return await page.evaluate(() => { return !!document.querySelector('ods-combobox')?.shadowRoot?.querySelector('.ods-combobox__search--empty'); }); } it('should add empty classes when the list is completely empty', async() => { await setup(''); await el.callMethod('open'); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await hasEmptyResultsClass()).toBe(true); expect(await hasEmptySearchClass()).toBe(true); }); it('should not add empty classes when items are available', async() => { await setup('Dummy'); await el.callMethod('open'); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await hasEmptyResultsClass()).toBe(false); expect(await hasEmptySearchClass()).toBe(false); }); it('should not add empty classes when "no results" message is displayed', async() => { await setup('Dummy'); await el.callMethod('open'); await page.waitForChanges(); await input.type('nonexistenttext', { delay: 100 }); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await hasEmptyResultsClass()).toBe(false); expect(await hasEmptySearchClass()).toBe(false); }); it('should not add empty classes when "add new" option is available', async() => { await setup('Dummy'); await el.callMethod('open'); await page.waitForChanges(); await input.type('newvalue', { delay: 100 }); await page.waitForChanges(); expect(await isOpen()).toBe(true); expect(await hasEmptyResultsClass()).toBe(false); expect(await hasEmptySearchClass()).toBe(false); }); }); });