import { expect, fixture as _fixture, fixtureSync as _fixtureSync, html, defineCE, unsafeStatic, aTimeout, } from '@open-wc/testing'; import sinon from 'sinon'; // @ts-ignore import { PhoneUtilManager } from '@lion/input-tel'; // @ts-ignore import { mockPhoneUtilManager, restorePhoneUtilManager } from '@lion/input-tel/test-helpers'; import { LionInputTelDropdown } from '../src/LionInputTelDropdown.js'; /** * @typedef {import('@lion/core').TemplateResult} TemplateResult * @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement * @typedef {import('../types').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel */ const fixture = /** @type {(arg: string | TemplateResult) => Promise} */ ( _fixture ); const fixtureSync = /** @type {(arg: string | TemplateResult) => LionInputTelDropdown} */ ( _fixtureSync ); /** * @param {DropdownElement | HTMLSelectElement} dropdownEl * @returns {string} */ function getDropdownValue(dropdownEl) { if ('modelValue' in dropdownEl) { return dropdownEl.modelValue; } return dropdownEl.value; } /** * @param {DropdownElement} dropdownEl * @param {string} value */ function mimicUserChangingDropdown(dropdownEl, value) { if ('modelValue' in dropdownEl) { // eslint-disable-next-line no-param-reassign dropdownEl.modelValue = value; dropdownEl.dispatchEvent( new CustomEvent('model-value-changed', { detail: { isTriggeredByUser: true } }), ); } else { // eslint-disable-next-line no-param-reassign dropdownEl.value = value; dropdownEl.dispatchEvent(new Event('change')); } } /** * @param {{ klass:LionInputTelDropdown }} config */ // @ts-expect-error export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdown }) { // @ts-ignore const tagName = defineCE(/** @type {* & HTMLElement} */ (class extends klass {})); const tag = unsafeStatic(tagName); describe('LionInputTelDropdown', () => { beforeEach(async () => { // Wait till PhoneUtilManager has been loaded await PhoneUtilManager.loadComplete; }); it('syncs value of dropdown on init if input has no value', async () => { const el = await fixture(html` <${tag}> `); expect(el.activeRegion).to.equal('GB'); expect(el.value).to.equal('+44'); expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal( 'GB', ); }); it('syncs value of dropdown on reset if input has no value', async () => { const el = await fixture(html` <${tag}> `); el.modelValue = '+31612345678'; await el.updateComplete; expect(el.activeRegion).to.equal('NL'); el.reset(); await el.updateComplete; expect(el.activeRegion).to.equal('GB'); expect(el.value).to.equal('+44'); }); it('syncs value of dropdown on init if input has no value does not influence interaction states', async () => { const el = await fixture(html` <${tag}> `); // TODO find out why its get dirty again // expect(el.dirty).to.be.false; expect(el.prefilled).to.be.false; }); it('syncs value of dropdown on reset also resets interaction states', async () => { const el = await fixture(html` <${tag}> `); el.modelValue = '+31612345678'; await el.updateComplete; expect(el.dirty).to.be.true; expect(el.prefilled).to.be.false; el.reset(); await el.updateComplete; expect(el.dirty).to.be.false; expect(el.prefilled).to.be.false; }); it('sets correct interaction states on init if input has a value', async () => { const el = await fixture(html` <${tag} .modelValue="${'+31612345678'}"> `); // TODO find out why its get dirty again // expect(el.dirty).to.be.false; expect(el.prefilled).to.be.true; }); describe('Dropdown display', () => { it('calls `templates.dropdown` with TemplateDataForDropdownInputTel object', async () => { const el = fixtureSync(html` <${tag} .modelValue="${'+31612345678'}" .allowedRegions="${['NL', 'PH']}" .preferredRegions="${['PH']}" > `); const spy = sinon.spy( /** @type {typeof LionInputTelDropdown} */ (el.constructor).templates, 'dropdown', ); await el.updateComplete; const dropdownNode = el.refs.dropdown.value; const templateDataForDropdown = /** @type {TemplateDataForDropdownInputTel} */ ( spy.args[0][0] ); expect(templateDataForDropdown).to.eql( /** @type {TemplateDataForDropdownInputTel} */ ({ data: { activeRegion: 'NL', regionMetaList: [ { countryCode: 31, flagSymbol: '🇳🇱', nameForLocale: 'Netherlands', nameForRegion: 'Nederland', regionCode: 'NL', }, ], regionMetaListPreferred: [ { countryCode: 63, flagSymbol: '🇵🇭', nameForLocale: 'Philippines', nameForRegion: 'Philippines', regionCode: 'PH', }, ], }, refs: { dropdown: { labels: { selectCountry: 'Select country', allCountries: 'All countries', preferredCountries: 'Suggested countries', }, listeners: { // @ts-expect-error [allow-protected] change: el._onDropdownValueChange, // @ts-expect-error [allow-protected] 'model-value-changed': el._onDropdownValueChange, }, props: { style: 'height: 100%;' }, ref: { value: dropdownNode }, }, }, }), ); spy.restore(); }); it('can override "all-countries-label"', async () => { const el = fixtureSync(html` <${tag} .preferredRegions="${['PH']}" > `); const spy = sinon.spy( /** @type {typeof LionInputTelDropdown} */ (el.constructor).templates, 'dropdown', ); // @ts-expect-error [allow-protected] el._allCountriesLabel = 'foo'; await el.updateComplete; const templateDataForDropdown = /** @type {TemplateDataForDropdownInputTel} */ ( spy.args[0][0] ); expect(templateDataForDropdown.refs.dropdown.labels.allCountries).to.eql('foo'); spy.restore(); }); it('can override "preferred-countries-label"', async () => { const el = fixtureSync(html` <${tag} .preferredRegions="${['PH']}" > `); const spy = sinon.spy( /** @type {typeof LionInputTelDropdown} */ (el.constructor).templates, 'dropdown', ); // @ts-expect-error [allow-protected] el._preferredCountriesLabel = 'foo'; await el.updateComplete; const templateDataForDropdown = /** @type {TemplateDataForDropdownInputTel} */ ( spy.args[0][0] ); expect(templateDataForDropdown.refs.dropdown.labels.preferredCountries).to.eql('foo'); spy.restore(); }); it('syncs dropdown value initially from activeRegion', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['DE']}"> `); expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal( 'DE', ); }); it('syncs disabled attribute to dropdown', async () => { const el = await fixture(html` <${tag} disabled> `); expect(el.refs.dropdown.value?.hasAttribute('disabled')).to.be.true; }); it('disables dropdown on readonly', async () => { const el = await fixture(html` <${tag} readonly> `); expect(el.refs.dropdown.value?.hasAttribute('disabled')).to.be.true; }); it('renders to prefix slot in light dom', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['DE']}"> `); const prefixSlot = /** @type {HTMLElement} */ ( /** @type {HTMLElement} */ (el.refs.dropdown.value).parentElement ); expect(prefixSlot.getAttribute('slot')).to.equal('prefix'); expect(prefixSlot.slot).to.equal('prefix'); expect(prefixSlot.parentElement).to.equal(el); }); it('rerenders light dom when PhoneUtil loaded', async () => { const { resolveLoaded } = mockPhoneUtilManager(); const el = await fixture(html` <${tag} .allowedRegions="${['DE']}"> `); // @ts-ignore const spy = sinon.spy(el, '_scheduleLightDomRender'); resolveLoaded(undefined); await aTimeout(0); expect(spy).to.have.been.calledOnce; restorePhoneUtilManager(); }); }); describe('On dropdown value change', () => { it('changes the currently active country code in the textbox', async () => { const el = await fixture( html` <${tag} .allowedRegions="${[ 'NL', 'BE', ]}" .modelValue="${'+31612345678'}"> `, ); // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; expect(el.activeRegion).to.equal('BE'); expect(el.modelValue).to.equal('+32612345678'); await el.updateComplete; expect(el.value).to.equal('+32612345678'); }); it('changes the currently active country code in the textbox when empty', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['NL', 'BE']}"> `); el.value = ''; // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; await el.updateComplete; expect(el.value).to.equal('+32'); }); it('changes the currently active country code in the textbox when invalid', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['NL', 'BE']}"> `); el.value = '+3'; // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; await el.updateComplete; expect(el.value).to.equal('+32'); }); it('changes the currently active country code in the textbox when invalid and small part of phone number', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['NL', 'BE']}"> `); el.value = '+3 2'; // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; await el.updateComplete; expect(el.value).to.equal('+32 2'); }); it('changes the currently active country code in the textbox when invalid and bigger part of phone number', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['NL', 'BE']}"> `); el.value = '+3 612345678'; // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; await el.updateComplete; expect(el.value).to.equal('+32 612345678'); }); it('changes the currently phonenumber completely in the textbox when not sure what to replace', async () => { const el = await fixture(html` <${tag} .allowedRegions="${['NL', 'BE']}""> `); el.value = '+9912345678'; // @ts-ignore mimicUserChangingDropdown(el.refs.dropdown.value, 'BE'); await el.updateComplete; await el.updateComplete; expect(el.value).to.equal('+32'); }); it('focuses the textbox right after selection if selected via opened dropdown', async () => { const el = await fixture( html` <${tag} .allowedRegions="${[ 'NL', 'BE', ]}" .modelValue="${'+31612345678'}"> `, ); const dropdownElement = el.refs.dropdown.value; // @ts-expect-error [allow-protected-in-tests] if (dropdownElement?._overlayCtrl) { // @ts-expect-error [allow-protected-in-tests] dropdownElement._overlayCtrl.show(); mimicUserChangingDropdown(dropdownElement, 'BE'); await el.updateComplete; await aTimeout(0); // @ts-expect-error [allow-protected-in-tests] expect(el._inputNode).to.equal(document.activeElement); } }); it('keeps focus on dropdownElement after selection if selected via unopened dropdown', async () => { const el = await fixture( html` <${tag} .allowedRegions="${[ 'NL', 'BE', ]}" .modelValue="${'+31612345678'}"> `, ); const dropdownElement = el.refs.dropdown.value; // @ts-ignore mimicUserChangingDropdown(dropdownElement, 'BE'); await el.updateComplete; // @ts-expect-error [allow-protected-in-tests] expect(el._inputNode).to.not.equal(document.activeElement); }); }); describe('On activeRegion change', () => { it('updates dropdown value ', async () => { const el = await fixture(html` <${tag} .modelValue="${'+31612345678'}"> `); expect(el.activeRegion).to.equal('NL'); // @ts-expect-error [allow protected] el._setActiveRegion('BE'); await el.updateComplete; expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal( 'BE', ); }); it('keeps dropdown value if countryCode is the same', async () => { const el = await fixture(html` <${tag} .modelValue="${'+12345678901'}"> `); expect(el.activeRegion).to.equal('US'); // @ts-expect-error [allow-protected-in test] el._setActiveRegion('AG'); // Also +1 await el.updateComplete; expect(getDropdownValue(/** @type {DropdownElement} */ (el.refs.dropdown.value))).to.equal( 'US', ); }); }); }); }