import { expect, Mock, vi } from 'vitest'; import Choices, { DEFAULT_CONFIG, ActionType, EventType, KeyCodeMap, InputChoice, InputGroup, Options, } from '../../src'; import { WrappedSelect, WrappedInput, Dropdown, Input } from '../../src/scripts/components/index'; import { removeItem } from '../../src/scripts/actions/items'; import templates from '../../src/scripts/templates'; import { ChoiceFull } from '../../src/scripts/interfaces/choice-full'; import { SearchByFuse } from '../../src/scripts/search/fuse'; import { SearchByKMP } from '../../src/scripts/search/kmp'; import { SearchByPrefixFilter } from '../../src/scripts/search/prefix-filter'; import { AnyAction, StoreListener } from '../../src/scripts/interfaces/store'; import Store from '../../src/scripts/store/store'; import { GroupFull } from '../../src/scripts/interfaces/group-full'; import { mapInputToChoice } from '../../src/scripts/lib/choice-input'; describe('choices', () => { let instance: Choices; let output: any; let passedElement: HTMLInputElement; beforeEach(() => { passedElement = document.createElement('input'); passedElement.type = 'text'; passedElement.className = 'js-choices'; document.body.appendChild(passedElement); instance = new Choices(passedElement, { allowHTML: true }); }); afterEach(() => { output = null; }); describe('constructor', () => { describe('config', () => { describe('not passing config options', () => { it('uses the default config', () => { document.body.innerHTML = ` `; instance = new Choices(); expect(instance.config).to.deep.equal({ ...DEFAULT_CONFIG, searchEnabled: false, closeDropdownOnSelect: true, renderSelectedChoices: false, searchRenderSelectedChoices: true, }); }); }); describe('passing config options', () => { it('merges the passed config with the default config', () => { document.body.innerHTML = ` `; const config = { allowHTML: true, renderChoiceLimit: 5, }; instance = new Choices('[data-choice]', config); expect(instance.config).to.deep.equal({ ...DEFAULT_CONFIG, searchEnabled: false, closeDropdownOnSelect: true, renderSelectedChoices: false, searchRenderSelectedChoices: true, ...config, }); }); describe('passing the searchEnabled config option with a value of false', () => { describe('passing a select-multiple element', () => { it('sets searchEnabled to true', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true, searchEnabled: true, }); expect(instance.config.searchEnabled).to.equal(true); }); it('sets searchEnabled to false', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true, searchEnabled: false, }); expect(instance.config.searchEnabled).to.equal(false); }); }); }); describe('passing the renderSelectedChoices config option with an unexpected value', () => { it('sets renderSelectedChoices to "auto"', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true, renderSelectedChoices: 'test' as any, }); expect(instance.config.renderSelectedChoices).to.equal(false); }); }); describe('passing the searchRenderSelectedChoices config option with false', () => { it('keeps searchRenderSelectedChoices as false', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true, searchRenderSelectedChoices: false, }); expect(instance.config.searchRenderSelectedChoices).to.equal(false); }); }); }); }); describe('not passing an element', () => { it('returns a Choices instance for the first element with a "data-choice" attribute', () => { document.body.innerHTML = ` `; const inputs = document.querySelectorAll('[data-choice]'); expect(inputs.length).to.equal(3); instance = new Choices(undefined, { allowHTML: true }); expect(instance.passedElement.element.id).to.equal(inputs[0].id); }); describe('when an element cannot be found in the DOM', () => { it('throws an error', () => { document.body.innerHTML = ``; expect(() => new Choices(undefined, { allowHTML: true })).to.throw( TypeError, 'Selector [data-choice] failed to find an element', ); }); }); describe('when an element is not of the expected type', () => { it('throws an error', () => { document.body.innerHTML = `
`; expect(() => new Choices(undefined, { allowHTML: true })).to.throw( TypeError, 'Selector [data-choice] failed to find an element', ); }); }); }); describe('passing an element', () => { describe('passing an element that has not been initialised with Choices', () => { beforeEach(() => { document.body.innerHTML = ` `; }); it('sets the initialised flag to true', () => { instance = new Choices('#input-1', { allowHTML: true }); expect(instance.initialised).to.equal(true); }); it('intialises', () => { const initSpy = vi.fn(); // initialise with the same element instance = new Choices('#input-1', { allowHTML: true, silent: true, callbackOnInit: initSpy, }); expect(initSpy).toHaveBeenCalled(); }); }); describe('passing an element that has already be initialised with Choices', () => { beforeEach(() => { document.body.innerHTML = ` `; // initialise once new Choices('#input-1', { allowHTML: true, silent: true }); }); it('sets the initialised flag to true', () => { // initialise with the same element instance = new Choices('#input-1', { allowHTML: true, silent: true }); expect(instance.initialised).to.equal(true); }); it('does not reinitialise', () => { const initSpy = vi.fn(); // initialise with the same element instance = new Choices('#input-1', { allowHTML: true, silent: true, callbackOnInit: initSpy, }); expect(initSpy).not.toHaveBeenCalled(); }); }); describe(`passing an element as a DOMString`, () => { describe('passing a input element type', () => { it('sets the "passedElement" instance property as an instance of WrappedInput', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true }); expect(instance.passedElement).to.be.an.instanceOf(WrappedInput); }); }); describe('passing a select element type', () => { it('sets the "passedElement" instance property as an instance of WrappedSelect', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true }); expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect); }); }); }); describe(`passing an element as a HTMLElement`, () => { describe('passing a input element type', () => { it('sets the "passedElement" instance property as an instance of WrappedInput', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true }); expect(instance.passedElement).to.be.an.instanceOf(WrappedInput); }); }); describe('passing a select element type', () => { it('sets the "passedElement" instance property as an instance of WrappedSelect', () => { document.body.innerHTML = ` `; instance = new Choices('[data-choice]', { allowHTML: true }); expect(instance.passedElement).to.be.an.instanceOf(WrappedSelect); }); }); }); describe('passing an invalid element type', () => { it('throws an TypeError', () => { document.body.innerHTML = `
`; expect(() => new Choices('[data-choice]', { allowHTML: true })).to.throw( TypeError, 'Expected one of the following types text|select-one|select-multiple', ); }); }); }); }); describe('public methods', () => { describe('init', () => { const callbackOnInitSpy = vi.fn(); beforeEach(() => { instance = new Choices(passedElement, { allowHTML: true, callbackOnInit: callbackOnInitSpy, silent: true, }); }); describe('when already initialised', () => { beforeEach(() => { instance.initialised = true; instance.init(); }); it('do not set initialise flag', () => { expect(instance.initialised).to.not.equal(false); }); }); describe('not already initialised', () => { let createTemplatesSpy: Mock<() => void>; let createInputSpy: Mock<() => void>; let storeSubscribeSpy: Mock<(onChange: StoreListener) => Store>; let renderSpy: Mock<() => void>; let addEventListenersSpy: Mock<() => void>; beforeEach(() => { createTemplatesSpy = vi.spyOn(instance, '_createTemplates'); createInputSpy = vi.spyOn(instance, '_createStructure'); storeSubscribeSpy = vi.spyOn(instance._store, 'subscribe'); renderSpy = vi.spyOn(instance, '_render'); addEventListenersSpy = vi.spyOn(instance, '_addEventListeners'); instance.initialised = false; instance.initialisedOK = undefined; instance.init(); }); afterEach(() => { vi.restoreAllMocks(); }); it('sets initialise flag', () => { expect(instance.initialised).to.equal(true); }); it('creates templates', () => { expect(createTemplatesSpy).toHaveBeenCalled(); }); it('creates input', () => { expect(createInputSpy).toHaveBeenCalled(); }); it('subscribes to store with render method', () => { expect(storeSubscribeSpy).toHaveBeenCalled(); expect(storeSubscribeSpy).lastCalledWith(instance._render); }); it('fire initial render with no items or choices', () => { expect(renderSpy).toHaveBeenCalled(); }); it('adds event listeners', () => { expect(addEventListenersSpy).toHaveBeenCalled(); }); it('fires callback', () => { expect(callbackOnInitSpy).toHaveBeenCalled(); }); }); }); describe('destroy', () => { beforeEach(() => { passedElement = document.createElement('input'); passedElement.type = 'text'; passedElement.className = 'js-choices'; document.body.appendChild(passedElement); instance = new Choices(passedElement, { allowHTML: true }); }); describe('not already initialised', () => { beforeEach(() => { instance.initialised = false; instance.initialisedOK = undefined; instance.destroy(); }); it('do not set initialise flag', () => { expect(instance.initialised).to.not.equal(true); }); }); describe('when already initialised', () => { let removeEventListenersSpy: Mock<() => void>; let passedElementRevealSpy: Mock<() => void>; let containerOuterUnwrapSpy: Mock<(element: HTMLElement) => void>; let clearStoreSpy: Mock<(clearOptions?: boolean) => Choices>; beforeEach(() => { removeEventListenersSpy = vi.spyOn(instance, '_removeEventListeners'); passedElementRevealSpy = vi.spyOn(instance.passedElement, 'reveal'); containerOuterUnwrapSpy = vi.spyOn(instance.containerOuter, 'unwrap'); clearStoreSpy = vi.spyOn(instance, 'clearStore'); instance.initialised = true; instance.destroy(); }); afterEach(() => { vi.restoreAllMocks(); }); it('removes event listeners', () => { expect(removeEventListenersSpy).toHaveBeenCalled(); }); it('reveals passed element', () => { expect(passedElementRevealSpy).toHaveBeenCalled(); }); it('reverts outer container', () => { expect(containerOuterUnwrapSpy).toHaveBeenCalled(); expect(containerOuterUnwrapSpy).lastCalledWith(instance.passedElement.element); }); it('clears store', () => { expect(clearStoreSpy).toHaveBeenCalled(); }); it('restes templates config', () => { expect(instance._templates).to.deep.equal(templates); }); it('resets initialise flag', () => { expect(instance.initialised).to.equal(false); }); }); }); describe('enable', () => { let passedElementEnableSpy: Mock<() => void>; let addEventListenersSpy: Mock<() => void>; let containerOuterEnableSpy: Mock<() => void>; let inputEnableSpy: Mock<() => void>; beforeEach(() => { addEventListenersSpy = vi.spyOn(instance, '_addEventListeners'); passedElementEnableSpy = vi.spyOn(instance.passedElement, 'enable'); containerOuterEnableSpy = vi.spyOn(instance.containerOuter, 'enable'); inputEnableSpy = vi.spyOn(instance.input, 'enable'); }); afterEach(() => { vi.restoreAllMocks(); }); describe('when already enabled', () => { beforeEach(() => { instance.passedElement.isDisabled = false; instance.containerOuter.isDisabled = false; output = instance.enable(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(passedElementEnableSpy).not.toHaveBeenCalled(); expect(addEventListenersSpy).not.toHaveBeenCalled(); expect(inputEnableSpy).not.toHaveBeenCalled(); expect(containerOuterEnableSpy).not.toHaveBeenCalled(); }); }); describe('when not already enabled', () => { beforeEach(() => { instance.passedElement.isDisabled = true; instance.containerOuter.isDisabled = true; instance.enable(); }); it('adds event listeners', () => { expect(addEventListenersSpy).toHaveBeenCalled(); }); it('enables input', () => { expect(inputEnableSpy).toHaveBeenCalled(); }); it('enables containerOuter', () => { expect(containerOuterEnableSpy).toHaveBeenCalled(); }); }); }); describe('disable', () => { let removeEventListenersSpy: Mock<() => void>; let passedElementDisableSpy: Mock<() => void>; let containerOuterDisableSpy: Mock<() => void>; let inputDisableSpy: Mock<() => void>; beforeEach(() => { removeEventListenersSpy = vi.spyOn(instance, '_removeEventListeners'); passedElementDisableSpy = vi.spyOn(instance.passedElement, 'disable'); containerOuterDisableSpy = vi.spyOn(instance.containerOuter, 'disable'); inputDisableSpy = vi.spyOn(instance.input, 'disable'); }); afterEach(() => { vi.restoreAllMocks(); }); describe('when already disabled', () => { beforeEach(() => { instance.passedElement.isDisabled = true; instance.containerOuter.isDisabled = true; output = instance.disable(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(removeEventListenersSpy).not.toHaveBeenCalled(); expect(passedElementDisableSpy).not.toHaveBeenCalled(); expect(containerOuterDisableSpy).not.toHaveBeenCalled(); expect(inputDisableSpy).not.toHaveBeenCalled(); }); }); describe('when not already disabled', () => { beforeEach(() => { instance.passedElement.isDisabled = false; instance.containerOuter.isDisabled = false; output = instance.disable(); }); it('removes event listeners', () => { expect(removeEventListenersSpy).toHaveBeenCalled(); }); it('disables input', () => { expect(inputDisableSpy).toHaveBeenCalled(); }); it('enables containerOuter', () => { expect(containerOuterDisableSpy).toHaveBeenCalled(); }); }); }); describe('showDropdown', () => { let containerOuterOpenSpy: Mock<(dropdownPos: number, dropdownHeight: number) => void>; let dropdownShowSpy: Mock<() => Dropdown>; let inputFocusSpy: Mock<() => void>; let passedElementTriggerEventStub: Mock; beforeEach(() => { containerOuterOpenSpy = vi.spyOn(instance.containerOuter, 'open'); dropdownShowSpy = vi.spyOn(instance.dropdown, 'show'); inputFocusSpy = vi.spyOn(instance.input, 'focus'); passedElementTriggerEventStub = vi.fn(); instance.passedElement.triggerEvent = passedElementTriggerEventStub; }); afterEach(() => { vi.restoreAllMocks(); }); describe('dropdown active', () => { beforeEach(() => { instance.dropdown.isActive = true; output = instance.showDropdown(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(containerOuterOpenSpy).not.toHaveBeenCalled(); expect(dropdownShowSpy).not.toHaveBeenCalled(); expect(inputFocusSpy).not.toHaveBeenCalled(); expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); }); }); describe('dropdown inactive', () => { beforeEach(() => { instance.dropdown.isActive = false; output = instance.showDropdown(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('focuses input synchronously when input focus is allowed', () => { instance.dropdown.isActive = false; instance.showDropdown(false); expect(inputFocusSpy).toHaveBeenCalledOnce(); }); it('opens containerOuter', () => new Promise((done) => { requestAnimationFrame(() => { expect(containerOuterOpenSpy).toHaveBeenCalled(); done(true); }); })); it('shows dropdown with blurInput flag', () => new Promise((done) => { requestAnimationFrame(() => { expect(dropdownShowSpy).toHaveBeenCalled(); done(true); }); })); it('triggers event on passedElement', () => new Promise((done) => { requestAnimationFrame(() => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.showDropdown); done(true); }); })); describe('passing true focusInput flag with canSearch set to true', () => { beforeEach(() => { instance.dropdown.isActive = false; instance._canSearch = true; output = instance.showDropdown(true); }); it('focuses input', () => new Promise((done) => { requestAnimationFrame(() => { expect(inputFocusSpy).not.toHaveBeenCalled(); done(true); }); })); }); }); }); describe('hideDropdown', () => { let containerOuterCloseSpy: Mock<() => void>; let dropdownHideSpy: Mock<() => Dropdown>; let inputBlurSpy: Mock<() => void>; let inputRemoveActiveDescendantSpy: Mock<() => void>; let passedElementTriggerEventStub: Mock; beforeEach(() => { containerOuterCloseSpy = vi.spyOn(instance.containerOuter, 'close'); dropdownHideSpy = vi.spyOn(instance.dropdown, 'hide'); inputBlurSpy = vi.spyOn(instance.input, 'blur'); inputRemoveActiveDescendantSpy = vi.spyOn(instance.input, 'removeActiveDescendant'); passedElementTriggerEventStub = vi.fn(); instance.passedElement.triggerEvent = passedElementTriggerEventStub; }); afterEach(() => { vi.restoreAllMocks(); }); describe('dropdown inactive', () => { beforeEach(() => { instance.dropdown.isActive = false; output = instance.hideDropdown(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(containerOuterCloseSpy).not.toHaveBeenCalled(); expect(dropdownHideSpy).not.toHaveBeenCalled(); expect(inputBlurSpy).not.toHaveBeenCalled(); expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); }); }); describe('dropdown active', () => { beforeEach(() => { instance.dropdown.isActive = true; output = instance.hideDropdown(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('closes containerOuter', () => new Promise((done) => { requestAnimationFrame(() => { expect(containerOuterCloseSpy).toHaveBeenCalled(); done(true); }); })); it('hides dropdown with blurInput flag', () => new Promise((done) => { requestAnimationFrame(() => { expect(dropdownHideSpy).toHaveBeenCalled(); done(true); }); })); it('triggers event on passedElement', () => new Promise((done) => { requestAnimationFrame(() => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.hideDropdown); done(true); }); })); describe('passing true blurInput flag with canSearch set to true', () => { beforeEach(() => { instance.dropdown.isActive = true; instance._canSearch = true; output = instance.hideDropdown(true); }); it('removes active descendants', () => new Promise((done) => { requestAnimationFrame(() => { expect(inputRemoveActiveDescendantSpy).toHaveBeenCalled(); done(true); }); })); it('blurs input', () => new Promise((done) => { requestAnimationFrame(() => { expect(inputBlurSpy).toHaveBeenCalled(); done(true); }); })); }); }); }); describe('highlightItem', () => { let passedElementTriggerEventStub: Mock; let storeDispatchSpy: Mock<(action: AnyAction) => void>; let storeGetGroupByIdStub: Mock; const groupIdValue = 'Test'; const item: ChoiceFull = { group: null, highlighted: false, active: false, disabled: false, placeholder: false, selected: false, id: 1234, value: 'Test', label: 'Test', score: 0, rank: 0, }; beforeEach(() => { vi.spyOn(instance._store, 'choices', 'get').mockImplementation(() => [item]); vi.spyOn(instance._store, 'items', 'get').mockImplementation(() => [item]); passedElementTriggerEventStub = vi.fn(); storeGetGroupByIdStub = vi.fn().mockImplementation(() => { return { id: 4321, label: groupIdValue, }; }); storeDispatchSpy = vi.spyOn(instance._store, 'dispatch'); instance._store.getGroupById = storeGetGroupByIdStub; instance.passedElement.triggerEvent = passedElementTriggerEventStub; }); afterEach(() => { vi.restoreAllMocks(); }); describe('no item passed', () => { beforeEach(() => { output = instance.highlightItem(null); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); expect(storeDispatchSpy).not.toHaveBeenCalled(); expect(storeGetGroupByIdStub).not.toHaveBeenCalled(); }); }); describe('item passed', () => { describe('passing truthy second paremeter', () => { beforeEach(() => { output = instance.highlightItem(item, true); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('dispatches highlightItem action with correct arguments', () => { expect(storeDispatchSpy).toHaveBeenCalled(); expect(storeDispatchSpy).lastCalledWith({ type: ActionType.HIGHLIGHT_ITEM, item, highlighted: true, }); }); }); describe('item with no group', () => { beforeEach(() => { item.group = null; output = instance.highlightItem(item); }); it('triggers event with null groupValue', () => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.highlightItem, { active: false, customProperties: undefined, disabled: false, element: undefined, groupValue: undefined, highlighted: false, id: 1234, keyCode: undefined, label: 'Test', labelClass: undefined, labelDescription: '', placeholder: false, value: 'Test', }); }); }); describe('item with group', () => { beforeEach(() => { item.group = { active: true, choices: [], disabled: false, element: undefined, groupEl: undefined, id: 4321, label: groupIdValue, }; output = instance.highlightItem(item); }); it('triggers event with groupValue', () => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.highlightItem, { active: false, customProperties: undefined, disabled: false, element: undefined, groupValue: 'Test', highlighted: false, id: 1234, keyCode: undefined, label: 'Test', labelClass: undefined, labelDescription: '', placeholder: false, value: 'Test', }); }); }); describe('passing falsey second paremeter', () => { beforeEach(() => { output = instance.highlightItem(item, false); }); it('do not trigger event', () => { expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); }); }); }); describe('unhighlightItem', () => { let passedElementTriggerEventStub: Mock; let storeDispatchSpy: Mock<(action: AnyAction) => void>; let storeGetGroupByIdStub: (id: number) => GroupFull | undefined; const groupIdValue = 'Test'; const item: ChoiceFull = { group: null, highlighted: true, active: false, disabled: false, placeholder: false, selected: false, id: 1234, value: 'Test', label: 'Test', score: 0, rank: 0, }; beforeEach(() => { vi.spyOn(instance._store, 'choices', 'get').mockImplementation(() => [item]); vi.spyOn(instance._store, 'items', 'get').mockImplementation(() => [item]); passedElementTriggerEventStub = vi.fn(); storeGetGroupByIdStub = vi.fn().mockImplementation(() => { return { id: 4321, label: groupIdValue, }; }); storeDispatchSpy = vi.spyOn(instance._store, 'dispatch'); instance._store.getGroupById = storeGetGroupByIdStub; instance.passedElement.triggerEvent = passedElementTriggerEventStub; }); afterEach(() => { vi.restoreAllMocks(); }); describe('no item passed', () => { beforeEach(() => { output = instance.unhighlightItem(null); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('returns early', () => { expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); expect(storeDispatchSpy).not.toHaveBeenCalled(); expect(storeGetGroupByIdStub).not.toHaveBeenCalled(); }); }); describe('item passed', () => { describe('passing truthy second parameter', () => { beforeEach(() => { output = instance.unhighlightItem(item, true); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('dispatches highlightItem action with correct arguments', () => { expect(storeDispatchSpy).toHaveBeenCalled(); expect(storeDispatchSpy).lastCalledWith({ type: ActionType.HIGHLIGHT_ITEM, item, highlighted: false, }); }); }); describe('item without group', () => { beforeEach(() => { item.group = null; output = instance.unhighlightItem(item); }); it('triggers event with null groupValue', () => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.unhighlightItem, { active: false, customProperties: undefined, disabled: false, element: undefined, groupValue: undefined, highlighted: true, id: 1234, keyCode: undefined, label: 'Test', labelClass: undefined, labelDescription: '', placeholder: false, value: 'Test', }); }); }); describe('item with group', () => { beforeEach(() => { item.group = { active: true, choices: [], disabled: false, element: undefined, groupEl: undefined, id: 4321, label: groupIdValue, }; output = instance.unhighlightItem(item); }); it('triggers event with groupValue', () => { expect(passedElementTriggerEventStub).toHaveBeenCalled(); expect(passedElementTriggerEventStub).lastCalledWith(EventType.unhighlightItem, { active: false, customProperties: undefined, disabled: false, element: undefined, groupValue: 'Test', highlighted: true, id: 1234, keyCode: undefined, label: 'Test', labelClass: undefined, labelDescription: '', placeholder: false, value: 'Test', }); }); }); describe('passing falsey second paremeter', () => { beforeEach(() => { output = instance.unhighlightItem(item, false); }); it('do not trigger event', () => { expect(passedElementTriggerEventStub).not.toHaveBeenCalled(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); }); }); }); describe('highlightAll', () => { let storeDispatchSpy: Mock<(action: AnyAction) => void>; const items: ChoiceFull[] = [ { id: 1, value: 'Test 1', highlighted: false, disabled: false, active: false, group: null, label: '', placeholder: false, selected: false, score: 0, rank: 0, }, { id: 2, value: 'Test 2', highlighted: false, disabled: false, active: false, group: null, label: '', placeholder: false, selected: false, score: 0, rank: 0, }, ]; beforeEach(() => { vi.spyOn(instance._store, 'choices', 'get').mockImplementation(() => items); vi.spyOn(instance._store, 'items', 'get').mockImplementation(() => items); storeDispatchSpy = vi.spyOn(instance._store, 'dispatch'); output = instance.highlightAll(); }); afterEach(() => { vi.restoreAllMocks(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('highlights each item in store', () => { expect(storeDispatchSpy).callCount(items.length); expect(storeDispatchSpy).toHaveBeenNthCalledWith(1, { type: ActionType.HIGHLIGHT_ITEM, item: items[0], highlighted: true, }); expect(storeDispatchSpy).lastCalledWith({ type: ActionType.HIGHLIGHT_ITEM, item: items[1], highlighted: true, }); }); }); describe('unhighlightAll', () => { let storeDispatchSpy: Mock<(action: AnyAction) => void>; const items: ChoiceFull[] = [ { id: 1, value: 'Test 1', highlighted: true, disabled: false, active: false, group: null, label: '', placeholder: false, selected: false, score: 0, rank: 0, }, { id: 2, value: 'Test 2', highlighted: true, disabled: false, active: false, group: null, label: '', placeholder: false, selected: false, score: 0, rank: 0, }, ]; beforeEach(() => { vi.spyOn(instance._store, 'choices', 'get').mockImplementation(() => items); vi.spyOn(instance._store, 'items', 'get').mockImplementation(() => items); storeDispatchSpy = vi.spyOn(instance._store, 'dispatch'); output = instance.unhighlightAll(); }); afterEach(() => { vi.restoreAllMocks(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('unhighlights each item in store', () => { expect(storeDispatchSpy).callCount(items.length); expect(storeDispatchSpy).toHaveBeenNthCalledWith(1, { type: ActionType.HIGHLIGHT_ITEM, item: items[0], highlighted: false, }); expect(storeDispatchSpy).lastCalledWith({ type: ActionType.HIGHLIGHT_ITEM, item: items[1], highlighted: false, }); }); }); describe('clearChoices', () => { let storeResetStub: Mock; beforeEach(() => { storeResetStub = vi.fn(); instance._store.reset = storeResetStub; output = instance.clearChoices(); }); afterEach(() => { vi.restoreAllMocks(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); it('dispatches clearChoices action', () => { expect(storeResetStub).callCount(1); }); }); describe('clearInput', () => { let inputClearSpy: Mock<(setWidth?: boolean) => Input>; let storeDispatchStub: Mock; beforeEach(() => { inputClearSpy = vi.spyOn(instance.input, 'clear'); storeDispatchStub = vi.fn(); instance._store.dispatch = storeDispatchStub; output = instance.clearInput(); }); afterEach(() => { vi.restoreAllMocks(); }); it('returns this', () => { expect(output).to.deep.equal(instance); }); describe('text element', () => { beforeEach(() => { instance._isSelectOneElement = false; instance._isTextElement = false; output = instance.clearInput(); }); it('clears input with correct arguments', () => { expect(inputClearSpy).toHaveBeenCalled(); expect(inputClearSpy).lastCalledWith(true); }); }); describe('select element with search enabled', () => { beforeEach(() => { instance._isSelectOneElement = true; instance._isTextElement = false; instance.config.searchEnabled = true; instance._isSearching = true; output = instance.clearInput(); }); it('clears input with correct arguments', () => { expect(inputClearSpy).toHaveBeenCalled(); expect(inputClearSpy).lastCalledWith(false); }); it('resets search flag', () => { expect(instance._isSearching).to.equal(false); }); it('dispatches activateChoices action', () => { expect(storeDispatchStub).toHaveBeenCalled(); expect(storeDispatchStub).lastCalledWith({ type: ActionType.ACTIVATE_CHOICES, active: true, }); }); }); }); describe('setChoices with callback/Promise', () => { describe('not initialised', () => { beforeEach(() => { instance.initialised = false; instance.initialisedOK = undefined; }); it('should throw', () => { // @ts-expect-error expect type error expect(() => instance.setChoices(null)).Throw(TypeError); }); }); describe('initialised twice', () => { it('throws', () => { instance.initialised = true; instance.initialisedOK = false; // @ts-expect-error expect type error expect(() => instance.setChoices(null)).to.throw( TypeError, 'setChoices called for an element which has multiple instances of Choices initialised on it', ); }); }); describe('text element', () => { beforeEach(() => { instance._isSelectElement = false; }); it('should throw', () => { // @ts-expect-error expect type error expect(() => instance.setChoices(null)).Throw(TypeError); }); }); describe('passing invalid function', () => { beforeEach(() => { instance._isSelectElement = true; }); it('should throw on non function', () => { // @ts-expect-error expect type error expect(() => instance.setChoices(null)).Throw(TypeError, /Promise/i); }); it(`should throw on function that doesn't return promise`, () => { // @ts-expect-error expect type error expect(() => instance.setChoices(() => 'boo')).to.throw(TypeError, /promise/i); }); }); describe('select element', () => { it('fetches and sets choices', async () => { document.body.innerHTML = ' `; instance = new Choices('[data-choice]', { choices, allowHTML: false, searchEnabled: true, }); }); describe('fuse', () => { beforeEach(() => { process.env.CHOICES_SEARCH_FUSE = 'full'; instance._searcher = new SearchByFuse(instance.config); }); it('details are passed', () => new Promise((done) => { const query = 'This is a query & a "test" with characters that should not be sanitised.'; instance.input.value = query; instance.input.focus(); instance.passedElement.element.addEventListener( 'search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: query, resultCount: 0, }); done(true); }, { once: true }, ); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); it('uses Fuse options', () => new Promise((done) => { instance.config.fuseOptions.isCaseSensitive = true; instance.config.fuseOptions.minMatchCharLength = 4; instance._searcher = new SearchByFuse(instance.config); instance.input.value = 'test'; instance.input.focus(); instance.passedElement.element.addEventListener( 'search', (event: CustomEvent) => { expect(event.detail.resultCount).to.eql(0); done(true); }, { once: true }, ); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); it('is fired with a searchFloor of 0', () => new Promise((done) => { instance.config.searchFloor = 0; instance.input.value = 'qwerty'; instance.input.focus(); instance.passedElement.element.addEventListener('search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: instance.input.value, resultCount: 0, }); done(true); }); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); }); describe('kmp', () => { beforeEach(() => { instance._searcher = new SearchByKMP(instance.config); }); it('details are passed', () => new Promise((done) => { const query = 'This is a query & a "test" with characters that should not be sanitised.'; instance.input.value = query; instance.input.focus(); instance.passedElement.element.addEventListener( 'search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: query, resultCount: 0, }); done(true); }, { once: true }, ); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); it('is fired with a searchFloor of 0', () => new Promise((done) => { instance.config.searchFloor = 0; instance.input.value = 'qwerty'; instance.input.focus(); instance.passedElement.element.addEventListener('search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: instance.input.value, resultCount: 0, }); done(true); }); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); }); describe('prefix-filter', () => { beforeEach(() => { instance._searcher = new SearchByPrefixFilter(instance.config); }); it('details are passed', () => new Promise((done) => { const query = 'This is a query & a "test" with characters that should not be sanitised.'; instance.input.value = query; instance.input.focus(); instance.passedElement.element.addEventListener( 'search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: query, resultCount: 0, }); done(true); }, { once: true }, ); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); it('is fired with a searchFloor of 0', () => new Promise((done) => { instance.config.searchFloor = 0; instance.input.value = 'qwerty'; instance.input.focus(); instance.passedElement.element.addEventListener('search', (event: CustomEvent) => { expect(event.detail).to.contains({ value: instance.input.value, resultCount: 0, }); done(true); }); instance._onKeyUp(/* { target: null, keyCode: null } */); instance._onInput(/* { target: null } */); })); }); }); }); describe('private methods', () => { describe('_generatePlaceholderValue', () => { describe('select element', () => { describe('when a placeholder option is defined', () => { it('returns the text value of the placeholder option', () => { const placeholderValue = 'I am a placeholder'; instance._isSelectElement = true; (instance.passedElement as WrappedSelect).placeholderOption = { text: placeholderValue, }; const value = instance._generatePlaceholderValue(); expect(value).to.equal(placeholderValue); }); }); describe('when a placeholder option is not defined', () => { it('returns null', () => { instance._isSelectElement = true; (instance.passedElement as WrappedSelect).placeholderOption = undefined; const value = instance._generatePlaceholderValue(); expect(value).to.equal(null); }); }); }); describe('text input', () => { describe('when the placeholder config option is set to true', () => { describe('when the placeholderValue config option is defined', () => { it('returns placeholderValue', () => { const placeholderValue = 'I am a placeholder'; instance._isSelectElement = false; instance.config.placeholder = true; instance.config.placeholderValue = placeholderValue; instance._hasNonChoicePlaceholder = true; const value = instance._generatePlaceholderValue(); expect(value).to.equal(placeholderValue); }); }); }); describe('when the placeholder config option is set to false', () => { it('returns null', () => { instance._isSelectElement = false; instance.config.placeholder = false; const value = instance._generatePlaceholderValue(); expect(value).to.equal(null); }); }); }); }); describe('_onKeyDown', () => { let items; let hasItems; let hasActiveDropdown; let hasFocussedInput; beforeEach(() => { instance.showDropdown = vi.fn(); instance._onSelectKey = vi.fn(); instance._onEnterKey = vi.fn(); instance._onEscapeKey = vi.fn(); instance._onDirectionKey = vi.fn(); instance._onDeleteKey = vi.fn(); ({ items } = instance._store); hasItems = instance.itemList.element.hasChildNodes(); hasActiveDropdown = instance.dropdown.isActive; hasFocussedInput = instance.input.isFocussed; }); describe('direction key', () => { const keyCodes = [ [KeyCodeMap.UP_KEY, 'ArrowUp'], [KeyCodeMap.DOWN_KEY, 'ArrowDown'], [KeyCodeMap.PAGE_UP_KEY, 'PageUp'], [KeyCodeMap.PAGE_DOWN_KEY, 'PageDown'], ]; keyCodes.forEach(([keyCode, key]) => { it(`calls _onDirectionKey with the expected arguments`, () => { const event = { keyCode, key, } as KeyboardEvent; instance._onKeyDown(event); expect(instance._onDirectionKey).to.have.been.calledWith(event, hasActiveDropdown); }); }); }); describe('select key', () => { it(`calls _onSelectKey with the expected arguments`, () => { const event = { keyCode: KeyCodeMap.A_KEY, key: 'A', } as KeyboardEvent; instance._onKeyDown(event); expect(instance._onSelectKey).to.have.been.calledWith(event, hasItems); }); }); describe('enter key', () => { it(`calls _onEnterKey with the expected arguments`, () => { const event = { keyCode: KeyCodeMap.ENTER_KEY, key: 'Enter', } as KeyboardEvent; instance._onKeyDown(event); expect(instance._onEnterKey).to.have.been.calledWith(event, hasActiveDropdown); }); }); describe('delete key', () => { // this is not an error; the constants are named the reverse of their assigned key names, according // to their actual values, which appear to conform to the Windows VK mappings: // 0x08 = 'Backspace', 0x2E = 'Delete' // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#editing_keys const keyCodes = [ [KeyCodeMap.DELETE_KEY, 'Backspace'], [KeyCodeMap.BACK_KEY, 'Delete'], ]; keyCodes.forEach(([keyCode, key]) => { it(`calls _onDeleteKey with the expected arguments`, () => { const event = { keyCode, key, } as KeyboardEvent; instance._onKeyDown(event); expect(instance._onDeleteKey).to.have.been.calledWith(event, items, hasFocussedInput); }); }); }); }); describe('_removeItem', () => { beforeEach(() => { instance._store.dispatch = vi.fn(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('when given an item to remove', () => { const item: ChoiceFull = { highlighted: false, active: false, disabled: false, placeholder: false, selected: false, id: 1111, value: 'test value', label: 'test label', group: null, customProperties: {}, score: 0, rank: 0, }; it('dispatches a REMOVE_ITEM action to the store', () => { instance._removeItem(item); expect(instance._store.dispatch).to.have.been.calledWith(removeItem(item)); }); it('triggers a REMOVE_ITEM event on the passed element', () => new Promise((done) => { passedElement.addEventListener( 'removeItem', (event: CustomEvent) => { expect(event.detail).to.contains({ id: item.id, value: item.value, label: item.label, customProperties: item.customProperties, groupValue: undefined, }); done(true); }, false, ); instance._removeItem(item); })); describe('when the item belongs to a group', () => { const group = { id: 1, label: 'testing', }; const itemWithGroup = { ...item, value: 'testing', group, } as ChoiceFull; beforeEach(() => { instance._store.getGroupById = vi.fn().mockImplementation(() => group); }); afterEach(() => { vi.restoreAllMocks(); }); it("includes the group's value in the triggered event", () => new Promise((done) => { passedElement.addEventListener( 'removeItem', (event: CustomEvent) => { expect(event.detail).to.contains({ id: itemWithGroup.id, value: itemWithGroup.value, label: itemWithGroup.label, customProperties: itemWithGroup.customProperties, groupValue: group.label, }); done(true); }, false, ); instance._removeItem(itemWithGroup); })); }); }); }); }); });