import '../../../dist/zn.min.js'; import {expect, fixture, html, waitUntil} from '@open-wc/testing'; import {clearFormStoreValues} from '../../utilities/form'; import type ZnInput from './input.component'; import type ZnSlashMenu from '../slash-menu/slash-menu.component'; describe('', () => { it('should render a component', async () => { const el = await fixture(html` `); expect(el).to.exist; }); describe('Escape key', () => { it('blurs the inner input and stops propagation when focused', async () => { const wrapper = await fixture(html`
`); const el = wrapper.querySelector('zn-input') as ZnInput; await el.updateComplete; let wrapperEscapes = 0; wrapper.addEventListener('keydown', (e) => { if ((e as KeyboardEvent).key === 'Escape') wrapperEscapes++; }); el.focus(); await el.updateComplete; expect(el.shadowRoot!.activeElement).to.equal(el.input); el.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, composed: true, cancelable: true })); await el.updateComplete; expect(el.shadowRoot!.activeElement, 'inner input should be blurred').to.not.equal(el.input); expect(wrapperEscapes, 'wrapper should not see Escape on press 1').to.equal(0); }); }); describe('trigger-submit', () => { const pressEnter = (el: ZnInput) => { el.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true })); }; it('submits an enter-navigation form on enter when trigger-submit is set', async () => { const form = await fixture(html`
`); const el = form.querySelector('zn-input')!; await el.updateComplete; let submitted = false; form.addEventListener('submit', e => { e.preventDefault(); submitted = true; }); el.focus(); pressEnter(el); expect(submitted).to.be.true; }); it('does not submit an enter-navigation form on enter without trigger-submit', async () => { const form = await fixture(html`
`); const el = form.querySelector('zn-input')!; await el.updateComplete; let submitted = false; form.addEventListener('submit', e => { e.preventDefault(); submitted = true; }); el.focus(); pressEnter(el); expect(submitted).to.be.false; }); }); describe('persisted state', () => { const formStoreKey = 'input-test-form-state'; const elementStoreKey = 'input-test-element-state'; beforeEach(() => { clearFormStoreValues(formStoreKey); clearFormStoreValues(elementStoreKey); }); afterEach(() => { clearFormStoreValues(formStoreKey); clearFormStoreValues(elementStoreKey); }); it('restores values from a form store-key after refresh', async () => { const firstForm = await fixture(html`
`); const firstInput = firstForm.querySelector('zn-input') as ZnInput; await firstInput.updateComplete; firstInput.value = 'Ada'; firstInput.dispatchEvent(new CustomEvent('zn-input', {bubbles: true, composed: true})); const secondForm = await fixture(html`
`); const secondInput = secondForm.querySelector('zn-input') as ZnInput; await secondInput.updateComplete; expect(secondInput.value).to.equal('Ada'); }); it('restores values from an element store-key without a form store-key', async () => { const firstInput = await fixture(html``); await firstInput.updateComplete; firstInput.value = 'Lovelace'; firstInput.dispatchEvent(new CustomEvent('zn-input', {bubbles: true, composed: true})); const secondInput = await fixture(html``); await secondInput.updateComplete; expect(secondInput.value).to.equal('Lovelace'); }); it('clears values for a form store-key', async () => { const firstForm = await fixture(html`
`); const firstInput = firstForm.querySelector('zn-input') as ZnInput; await firstInput.updateComplete; firstInput.value = 'Grace'; firstInput.dispatchEvent(new CustomEvent('zn-input', {bubbles: true, composed: true})); clearFormStoreValues(firstForm); const secondForm = await fixture(html`
`); const secondInput = secondForm.querySelector('zn-input') as ZnInput; await secondInput.updateComplete; expect(secondInput.value).to.equal(''); }); }); describe('type="range"', () => { const contextNote = (el: ZnInput) => el.shadowRoot!.querySelector('.form-control__label-context-note')!.textContent!.trim(); it('adopts the native midpoint when no value is set', async () => { const el = await fixture(html``); await el.updateComplete; expect(el.value).to.equal('5'); }); it('displays the value with its suffix in the context note', async () => { const el = await fixture( html`` ); await el.updateComplete; expect(contextNote(el)).to.equal('0.5rem'); }); it('updates the context note as the value changes', async () => { const el = await fixture( html`` ); await el.updateComplete; const input = el.shadowRoot!.querySelector('.input__control') as HTMLInputElement; input.value = '7'; input.dispatchEvent(new Event('input')); await el.updateComplete; expect(el.value).to.equal('7'); expect(contextNote(el)).to.equal('7px'); }); it('prefers an explicit context-note over the value', async () => { const el = await fixture( html`` ); await el.updateComplete; expect(contextNote(el)).to.equal('Custom'); }); }); describe('slash menu', () => { function slashMenuOf(el: ZnInput) { return el.shadowRoot!.querySelector('zn-slash-menu'); } /** Types `text` into the field the way a user would, leaving the caret at the end. */ function type(el: ZnInput, text: string) { el.input.value = text; el.input.setSelectionRange(text.length, text.length); el.input.dispatchEvent(new InputEvent('input', {bubbles: true, composed: true})); } async function openSlashMenu(el: ZnInput, text = '/') { await el.updateComplete; el.focus(); type(el, text); await waitUntil(() => slashMenuOf(el)?.open, 'the slash menu never opened'); return slashMenuOf(el)!; } it('stays closed when no items are configured', async () => { const el = await fixture(html``); await el.updateComplete; el.focus(); type(el, '/'); await el.updateComplete; await el.updateComplete; expect(slashMenuOf(el)?.open ?? false, 'nothing to insert, so nothing to show').to.be.false; }); it('opens on the trigger with items from the slash-items attribute', async () => { const el = await fixture(html` `); const menu = await openSlashMenu(el); expect(menu.items.map(item => item.label)).to.deep.equal(['Brand name', 'Support email']); }); it('collects items from slotted zn-slash-item elements', async () => { const el = await fixture(html` `); const menu = await openSlashMenu(el); expect(menu.items.map(item => item.label)).to.deep.equal(['Brand name']); }); it('inserts the active item on Enter, replacing the trigger and query', async () => { const el = await fixture(html` `); await openSlashMenu(el, 'Welcome to /bra'); el.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true })); await el.updateComplete; expect(el.value).to.equal('Welcome to {{BRAND_NAME}}'); }); it('does not submit the form when Enter chooses an item', async () => { const form = await fixture(html`
`); const el = form.querySelector('zn-input')!; let submits = 0; form.addEventListener('submit', event => { event.preventDefault(); submits++; }); await openSlashMenu(el); el.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true, cancelable: true })); await el.updateComplete; expect(el.value, 'the token should have replaced the trigger').to.equal('{{BRAND_NAME}}'); expect(submits, 'Enter belongs to the open menu, not the form').to.equal(0); }); it('closes on Escape without blurring the field', async () => { const el = await fixture(html` `); const menu = await openSlashMenu(el); el.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, composed: true, cancelable: true })); await el.updateComplete; expect(menu.open, 'Escape should close the menu').to.be.false; expect(el.shadowRoot!.activeElement, 'the field should keep focus').to.equal(el.input); }); }); });