From 825fed01c0f799ae41065b53298fc2b48c1d2fa9 Mon Sep 17 00:00:00 2001 From: Data5tream <6423657+Data5tream@users.noreply.github.com> Date: Mon, 18 Mar 2024 19:15:44 +0100 Subject: [PATCH 01/12] Add unit tests for helpers Add npm script for jest with coverage for easier testing and add some documentation to helpers.ts --- package.json | 3 +- src/slim-select/helpers.test.ts | 160 ++++++++++++++++++++++++++++++++ src/slim-select/helpers.ts | 54 ++++++++--- 3 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 src/slim-select/helpers.test.ts diff --git a/package.json b/package.json index c823dfcd..0a1c1158 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "build:library:js": "cd src/slim-select && rollup --config ./rollup.config.mjs && cd ../../", "build:library:css": "cd src/slim-select && sass ./slimselect.scss ../../dist/slimselect.css --style=compressed && cd ../../", "build:frameworks": "npm run build --workspaces", - "test": "jest" + "test": "jest", + "test:coverage": "jest --coverage" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/src/slim-select/helpers.test.ts b/src/slim-select/helpers.test.ts new file mode 100644 index 00000000..fc0f4211 --- /dev/null +++ b/src/slim-select/helpers.test.ts @@ -0,0 +1,160 @@ +/** + * @jest-environment jsdom + */ + +'use strict' +import { describe, expect, test } from '@jest/globals' +import { hasClassInTree, debounce, isEqual, kebabCase } from './helpers' + +describe('helpers module', () => { + describe('hasClassInTree', () => { + test('single element does not have class', () => { + const element = document.createElement('div') + element.className = 'invalid-class' + + expect(hasClassInTree(element, 'test-class')).toBeNull() + }) + + test('single element has class', () => { + const element = document.createElement('div') + element.className = 'test-class' + + expect(hasClassInTree(element, 'test-class')).toBe(element) + + element.classList.add('class-2', 'class-3') + expect(hasClassInTree(element, 'test-class')).toBe(element) + }) + + test('single element has class as data id', () => { + const element = document.createElement('div') + element.dataset.id = 'test-class' + + expect(hasClassInTree(element, 'test-class')).toBe(element) + + element.classList.add('class-2', 'class-3') + expect(hasClassInTree(element, 'test-class')).toBe(element) + }) + + test('parent element does not have class', () => { + const element = document.createElement('div') + element.className = 'invalid-class' + const child = document.createElement('div') + element.appendChild(child) + + expect(hasClassInTree(child, 'test-class')).toBeNull() + }) + + test('parent element has class', () => { + const element = document.createElement('div') + element.className = 'test-class' + const child = document.createElement('div') + element.appendChild(child) + + expect(hasClassInTree(child, 'test-class')).toBe(element) + + element.classList.add('class-2', 'class-3') + expect(hasClassInTree(child, 'test-class')).toBe(element) + }) + + test('parent element has class as data id', () => { + const element = document.createElement('div') + element.dataset.id = 'test-class' + + const child = document.createElement('div') + element.appendChild(child) + + expect(hasClassInTree(child, 'test-class')).toBe(element) + + element.classList.add('class-2', 'class-3') + expect(hasClassInTree(child, 'test-class')).toBe(element) + }) + }) + + describe('debounce', () => { + test('debounce calls function after default timeout', async () => { + const callback = jest.fn() + const debounced_function = debounce(callback) + debounced_function() + + await new Promise(r => setTimeout(r, 100)) + + expect(callback).toHaveBeenCalled() + expect(callback).toHaveBeenCalledTimes(1) + }) + + test('debounce calls function after higher timeout', async () => { + const callback = jest.fn() + const debounced_function = debounce(callback, 100) + debounced_function() + + await new Promise(r => setTimeout(r, 50)) + expect(callback).not.toHaveBeenCalled() + + await new Promise(r => setTimeout(r, 50)) + expect(callback).toHaveBeenCalled() + expect(callback).toHaveBeenCalledTimes(1) + }) + + test('debounce calls function after lower timeout', async () => { + const callback = jest.fn() + const debounced_function = debounce(callback, 10) + debounced_function() + + await new Promise(r => setTimeout(r, 5)) + expect(callback).not.toHaveBeenCalled() + + await new Promise(r => setTimeout(r, 5)) + expect(callback).toHaveBeenCalled() + expect(callback).toHaveBeenCalledTimes(1) + }) + + test('debounce respects inmediate setting', () => { + const callback = jest.fn() + const debounced_function = debounce(callback, 1000, true) + debounced_function() + + expect(callback).toHaveBeenCalled() + expect(callback).toHaveBeenCalledTimes(1) + }) + + test('debounce respects order of calls', async () => { + const callback = jest.fn(() => { + }) + const debounced_function: (a: number) => void = debounce(callback) + debounced_function(0) + debounced_function(1) + + await new Promise(r => setTimeout(r, 50)) + expect(callback).toHaveBeenCalledTimes(1) + expect(callback.mock.calls[0]).toStrictEqual([1]) + }) + }) + + describe('isEqual', () => { + test('different objects are not equal', () => { + expect(isEqual({ a: 1 }, { b: 1 })).toBe(false) + }) + + test('equal objects are equal', () => { + expect(isEqual({ a: 1 }, { a: 1 })).toBe(true) + }) + + test('more complex objects are equal', () => { + expect(isEqual({ a: 1, b: { c: 'asdf' } }, { a: 1, b: { c: 'asdf' } })).toBe(true) + }) + }) + + describe('kebabCase', () => { + test('kebab-case string', () => { + expect(kebabCase('kebab-case')).toBe('kebab-case') + }) + + test('camelCase string', () => { + expect(kebabCase('camelCase')).toBe('camel-case') + }) + + test('string with initial uppercase', () => { + expect(kebabCase('UpperCase')).toBe('upper-case') + }) + }) +}) diff --git a/src/slim-select/helpers.ts b/src/slim-select/helpers.ts index c6c1d40d..8c0742b5 100644 --- a/src/slim-select/helpers.ts +++ b/src/slim-select/helpers.ts @@ -1,9 +1,21 @@ -// Generate an 8 character random string +/** + * Generate an 8 character random string + * + * @returns a random 8 character string + */ export function generateID(): string { return Math.random().toString(36).substring(2, 10) } -export function hasClassInTree(element: HTMLElement, className: string) { +/** + * Check if a className exists on an element or any parent of the element + * + * @param element - the element to check + * @param className - the class name we are looking for + * + * @returns the element that has the class or `null` if the class was not found + */ +export function hasClassInTree(element: HTMLElement, className: string): HTMLElement | null { function hasClass(e: HTMLElement, c: string) { // If the element has the class return element if (c && e && e.classList && e.classList.contains(c)) { @@ -31,10 +43,16 @@ export function hasClassInTree(element: HTMLElement, className: string) { return hasClass(element, className) || parentByClass(element, className) } -// debounce will call the last requested function after the wait time +/** + * Executes the last function call after the wait time (or instantaneous if `immediate` is `true`) + * + * @param func - function that should be called after the wait time + * @param [wait=50] - wait time in ms + * @param [immediate=false] - if true, execute the function immediately instead of waiting + */ export function debounce void>(func: T, wait = 50, immediate = false): () => void { let timeout: any - return function (this: any, ...args: any[]): void { + return function(this: any, ...args: any[]): void { const context = self const later = () => { timeout = null @@ -51,20 +69,26 @@ export function debounce void>(func: T, wait = 50, } } -// reverseDebounce will call the function on the first call and then debounce -function reverseDebounce void>(func: T, timeout: number): T { - let timer: NodeJS.Timeout | null = null - return function (...args: any[]): void { - if (!timer) func(...args) - timer = setTimeout(() => (timer = null), timeout) - } as T -} - -export function isEqual(a: any, b: any) { +/** + * Compares two objects by comparing their JSON representations + * + * @param a - first object + * @param b - second object + * + * @returns `true` if `a` and `b` JSON representations are equal, `false` otherwise + */ +export function isEqual(a: any, b: any): boolean { return JSON.stringify(a) === JSON.stringify(b) } -export function kebabCase(str: string) { +/** + * Converts an input string into kebabCase + * + * @param str - input string + * + * @returns the input string in kebabCase + */ +export function kebabCase(str: string): string { const result = str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase()) return str[0] === str[0].toUpperCase() ? result.substring(1) : result } From 275a835a04e2daa12100cdee0a15a6c217314553 Mon Sep 17 00:00:00 2001 From: Data5tream <6423657+Data5tream@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:24:08 +0100 Subject: [PATCH 02/12] Add unit tests for CssClasses and Settings --- src/slim-select/css_classes.test.ts | 71 ++++++++++ src/slim-select/css_classes.ts | 192 ++++++++++++++-------------- src/slim-select/settings.test.ts | 84 ++++++++++++ 3 files changed, 251 insertions(+), 96 deletions(-) create mode 100644 src/slim-select/css_classes.test.ts create mode 100644 src/slim-select/settings.test.ts diff --git a/src/slim-select/css_classes.test.ts b/src/slim-select/css_classes.test.ts new file mode 100644 index 00000000..3128aaf7 --- /dev/null +++ b/src/slim-select/css_classes.test.ts @@ -0,0 +1,71 @@ +'use strict' +import { describe, expect, test } from '@jest/globals' +import CssClasses from './css_classes' + +const defaultClasses: { [key: string]: string } = { + main: 'ss-main', + placeholder: 'ss-placeholder', + values: 'ss-values', + single: 'ss-single', + max: 'ss-max', + value: 'ss-value', + valueText: 'ss-value-text', + valueDelete: 'ss-value-delete', + valueOut: 'ss-value-out', + deselect: 'ss-deselect', + deselectPath: 'M10,10 L90,90 M10,90 L90,10', + arrow: 'ss-arrow', + arrowClose: 'M10,30 L50,70 L90,30', + arrowOpen: 'M10,70 L50,30 L90,70', + content: 'ss-content', + openAbove: 'ss-open-above', + openBelow: 'ss-open-below', + search: 'ss-search', + searchHighlighter: 'ss-search-highlight', + searching: 'ss-searching', + addable: 'ss-addable', + addablePath: 'M50,10 L50,90 M10,50 L90,50', + list: 'ss-list', + optgroup: 'ss-optgroup', + optgroupLabel: 'ss-optgroup-label', + optgroupLabelText: 'ss-optgroup-label-text', + optgroupActions: 'ss-optgroup-actions', + optgroupSelectAll: 'ss-selectall', + optgroupSelectAllBox: 'M60,10 L10,10 L10,90 L90,90 L90,50', + optgroupSelectAllCheck: 'M30,45 L50,70 L90,10', + optgroupClosable: 'ss-closable', + option: 'ss-option', + optionDelete: 'M10,10 L90,90 M10,90 L90,10', + highlighted: 'ss-highlighted', + open: 'ss-open', + close: 'ss-close', + selected: 'ss-selected', + error: 'ss-error', + disabled: 'ss-disabled', + hide: 'ss-hide', +} + +describe('CssClasses module', () => { + test('empty constructor returns default classes', () => { + // We test the traditional classes here. Since old versions of slim-select didn't allow CSS class overrides, users + // may have written their CSS to match those classes, which means we should never change them in the library. + + // Convert to unknown and then to custom object to prevent TS from throwing errors + const classes = new CssClasses() as unknown as { [key: string]: string } + Object.keys(defaultClasses).forEach((key) => { + expect(classes[key]).toBe(defaultClasses[key]) + }) + }) + + test('classes can be overwritten via the constructor', () => { + const classesWithOverride = JSON.parse(JSON.stringify(defaultClasses)) + classesWithOverride['main'] = 'new-main' + classesWithOverride['open'] = 'new-open' + + // Convert to unknown and then to custom object to prevent TS from throwing errors + const classes = new CssClasses({ main: 'new-main', open: 'new-open' }) as unknown as { [key: string]: string } + Object.keys(classesWithOverride).forEach((key) => { + expect(classes[key]).toBe(classesWithOverride[key]) + }) + }) +}) diff --git a/src/slim-select/css_classes.ts b/src/slim-select/css_classes.ts index 7d329656..82c2f50f 100644 --- a/src/slim-select/css_classes.ts +++ b/src/slim-select/css_classes.ts @@ -1,111 +1,111 @@ export type CssClassesPartial = Partial export default class CssClasses { - public main: string - // Placeholder - public placeholder: string + public main: string + // Placeholder + public placeholder: string - // Values - public values: string - public single: string - public max: string - public value: string - public valueText: string - public valueDelete: string - public valueOut: string + // Values + public values: string + public single: string + public max: string + public value: string + public valueText: string + public valueDelete: string + public valueOut: string - // Deselect - public deselect: string - public deselectPath: string // Not a class but whatever + // Deselect + public deselect: string + public deselectPath: string // Not a class but whatever - // Arrow - public arrow: string - public arrowClose: string // Not a class but whatever - public arrowOpen: string // Not a class but whatever + // Arrow + public arrow: string + public arrowClose: string // Not a class but whatever + public arrowOpen: string // Not a class but whatever - // Content - public content: string - public openAbove: string - public openBelow: string + // Content + public content: string + public openAbove: string + public openBelow: string - // Search - public search: string - public searchHighlighter: string - public searching: string - public addable: string - public addablePath: string // Not a class but whatever + // Search + public search: string + public searchHighlighter: string + public searching: string + public addable: string + public addablePath: string // Not a class but whatever - // List optgroups/options - public list: string + // List optgroups/options + public list: string - // Optgroup - public optgroup: string - public optgroupLabel: string - public optgroupLabelText: string - public optgroupActions: string - public optgroupSelectAll: string // optgroup select all - public optgroupSelectAllBox: string // Not a class but whatever - public optgroupSelectAllCheck: string // Not a class but whatever - public optgroupClosable: string + // Optgroup + public optgroup: string + public optgroupLabel: string + public optgroupLabelText: string + public optgroupActions: string + public optgroupSelectAll: string // optgroup select all + public optgroupSelectAllBox: string // Not a class but whatever + public optgroupSelectAllCheck: string // Not a class but whatever + public optgroupClosable: string - // Option - public option: string - public optionDelete: string // Not a class but whatever - public highlighted: string + // Option + public option: string + public optionDelete: string // Not a class but whatever + public highlighted: string - // Misc - public open: string - public close: string - public selected: string - public error: string - public disabled: string - public hide: string - - constructor(classes?: CssClassesPartial) { - if (!classes) { - classes = {} - } - - this.main = classes.main || 'ss-main' - this.placeholder = classes.placeholder || 'ss-placeholder' - this.values = classes.values || 'ss-values' - this.single = classes.single || 'ss-single' - this.max = classes.max || 'ss-max' - this.value = classes.value || 'ss-value' - this.valueText = classes.valueText || 'ss-value-text' - this.valueDelete = classes.valueDelete || 'ss-value-delete' - this.valueOut = classes.valueOut || 'ss-value-out' + // Misc + public open: string + public close: string + public selected: string + public error: string + public disabled: string + public hide: string - this.deselect = classes.deselect || 'ss-deselect' - this.deselectPath = classes.deselectPath || 'M10,10 L90,90 M10,90 L90,10' - this.arrow = classes.arrow || 'ss-arrow' - this.arrowClose = classes.arrowClose || 'M10,30 L50,70 L90,30' - this.arrowOpen = classes.arrowOpen || 'M10,70 L50,30 L90,70' - this.content = classes.content || 'ss-content' - this.openAbove = classes.openAbove || 'ss-open-above' - this.openBelow = classes.openBelow || 'ss-open-below' - this.search = classes.search || 'ss-search' - this.searchHighlighter = classes.searchHighlighter || 'ss-search-highlight' - this.searching = classes.searching || 'ss-searching' - this.addable = classes.addable || 'ss-addable' - this.addablePath = classes.addablePath || 'M50,10 L50,90 M10,50 L90,50' - this.list = classes.list || 'ss-list' - this.optgroup = classes.optgroup || 'ss-optgroup' - this.optgroupLabel = classes.optgroupLabel || 'ss-optgroup-label' - this.optgroupLabelText = classes.optgroupLabelText || 'ss-optgroup-label-text' - this.optgroupActions = classes.optgroupActions || 'ss-optgroup-actions' - this.optgroupSelectAll = classes.optgroupSelectAll || 'ss-selectall' - this.optgroupSelectAllBox = classes.optgroupSelectAllBox || 'M60,10 L10,10 L10,90 L90,90 L90,50' - this.optgroupSelectAllCheck = classes.optgroupSelectAllCheck || 'M30,45 L50,70 L90,10' - this.optgroupClosable = classes.optgroupClosable || 'ss-closable' - this.option = classes.option || 'ss-option' - this.optionDelete = classes.optionDelete || 'M10,10 L90,90 M10,90 L90,10' - this.highlighted = classes.highlighted || 'ss-highlighted' - this.open = classes.open || 'ss-open' - this.close = classes.close || 'ss-close' - this.selected = classes.selected || 'ss-selected' - this.error = classes.error || 'ss-error' - this.disabled = classes.disabled || 'ss-disabled' - this.hide = classes.hide || 'ss-hide' + constructor(classes?: CssClassesPartial) { + if (!classes) { + classes = {} } + + this.main = classes.main || 'ss-main' + this.placeholder = classes.placeholder || 'ss-placeholder' + this.values = classes.values || 'ss-values' + this.single = classes.single || 'ss-single' + this.max = classes.max || 'ss-max' + this.value = classes.value || 'ss-value' + this.valueText = classes.valueText || 'ss-value-text' + this.valueDelete = classes.valueDelete || 'ss-value-delete' + this.valueOut = classes.valueOut || 'ss-value-out' + + this.deselect = classes.deselect || 'ss-deselect' + this.deselectPath = classes.deselectPath || 'M10,10 L90,90 M10,90 L90,10' + this.arrow = classes.arrow || 'ss-arrow' + this.arrowClose = classes.arrowClose || 'M10,30 L50,70 L90,30' + this.arrowOpen = classes.arrowOpen || 'M10,70 L50,30 L90,70' + this.content = classes.content || 'ss-content' + this.openAbove = classes.openAbove || 'ss-open-above' + this.openBelow = classes.openBelow || 'ss-open-below' + this.search = classes.search || 'ss-search' + this.searchHighlighter = classes.searchHighlighter || 'ss-search-highlight' + this.searching = classes.searching || 'ss-searching' + this.addable = classes.addable || 'ss-addable' + this.addablePath = classes.addablePath || 'M50,10 L50,90 M10,50 L90,50' + this.list = classes.list || 'ss-list' + this.optgroup = classes.optgroup || 'ss-optgroup' + this.optgroupLabel = classes.optgroupLabel || 'ss-optgroup-label' + this.optgroupLabelText = classes.optgroupLabelText || 'ss-optgroup-label-text' + this.optgroupActions = classes.optgroupActions || 'ss-optgroup-actions' + this.optgroupSelectAll = classes.optgroupSelectAll || 'ss-selectall' + this.optgroupSelectAllBox = classes.optgroupSelectAllBox || 'M60,10 L10,10 L10,90 L90,90 L90,50' + this.optgroupSelectAllCheck = classes.optgroupSelectAllCheck || 'M30,45 L50,70 L90,10' + this.optgroupClosable = classes.optgroupClosable || 'ss-closable' + this.option = classes.option || 'ss-option' + this.optionDelete = classes.optionDelete || 'M10,10 L90,90 M10,90 L90,10' + this.highlighted = classes.highlighted || 'ss-highlighted' + this.open = classes.open || 'ss-open' + this.close = classes.close || 'ss-close' + this.selected = classes.selected || 'ss-selected' + this.error = classes.error || 'ss-error' + this.disabled = classes.disabled || 'ss-disabled' + this.hide = classes.hide || 'ss-hide' + } } diff --git a/src/slim-select/settings.test.ts b/src/slim-select/settings.test.ts new file mode 100644 index 00000000..f98ac208 --- /dev/null +++ b/src/slim-select/settings.test.ts @@ -0,0 +1,84 @@ +'use strict' +import { describe, expect, test } from '@jest/globals' +import Settings from './settings' + +const defaultSettings: { [key: string]: any } = { + id: 'ss-qucyuytu', + style: '', + class: [], + isMultiple: false, + isOpen: false, + isFullOpen: false, + intervalMove: null, + disabled: false, + alwaysOpen: false, + showSearch: true, + focusSearch: true, + ariaLabel: 'Combobox', + searchPlaceholder: 'Search', + searchText: 'No Results', + searchingText: 'Searching...', + searchHighlight: false, + closeOnSelect: true, + contentLocation: HTMLBodyElement, + contentPosition: 'absolute', + openPosition: 'auto', + placeholderText: 'Select Value', + allowDeselect: false, + hideSelected: false, + keepOrder: false, + showOptionTooltips: false, + minSelected: 0, + maxSelected: 1000, + timeoutDelay: 200, + maxValuesShown: 20, + maxValuesMessage: '{number} selected', +} + +describe('Settings module', () => { + test('empty constructor returns default settings', () => { + // Convert to unknown and then to custom object to prevent TS from throwing errors + const settings = new Settings() as unknown as { [key: string]: string } + Object.keys(defaultSettings).forEach((key) => { + if (key === 'id') { + expect(settings[key].substring(0, 3)).toBe('ss-') + } else if (key === 'contentLocation') { + expect(settings[key]).toBeInstanceOf(defaultSettings[key]) + } else { + expect(settings[key]).toStrictEqual(defaultSettings[key]) + } + }) + }) + + test('settings can be overwritten via the constructor', () => { + const customSettings = { + disabled: true, + alwaysOpen: true, + showSearch: false, + focusSearch: true, + searchHighlight: true, + closeOnSelect: false, + placeholderText: 'new placeholder', + hideSelected: true, + keepOrder: true, + showOptionTooltips: true, + } + + const settingsWithOverride = { + ...defaultSettings, + ...customSettings, + } as unknown as { [key: string]: any } + + // Convert to unknown and then to custom object to prevent TS from throwing errors + const settings = new Settings(customSettings) as unknown as { [key: string]: any } + Object.keys(settingsWithOverride).forEach((key) => { + if (key === 'id') { + expect(settings[key].substring(0, 3)).toBe('ss-') + } else if (key === 'contentLocation') { + expect(settings[key]).toBeInstanceOf(defaultSettings[key]) + } else { + expect(settings[key]).toStrictEqual(settingsWithOverride[key]) + } + }) + }) +}) From 978950e17797f3f34eceed611720dd529cb2c6df Mon Sep 17 00:00:00 2001 From: Data5tream <6423657+Data5tream@users.noreply.github.com> Date: Wed, 20 Mar 2024 07:08:02 +0100 Subject: [PATCH 03/12] Fix bug in validateDataArray and add unit tests Add and improve unit tests for the Store class --- src/slim-select/store.test.ts | 1201 ++++++++++++++++++++++++++------- src/slim-select/store.ts | 34 +- 2 files changed, 974 insertions(+), 261 deletions(-) diff --git a/src/slim-select/store.test.ts b/src/slim-select/store.test.ts index 0fcdb436..13a9b830 100644 --- a/src/slim-select/store.test.ts +++ b/src/slim-select/store.test.ts @@ -1,242 +1,973 @@ 'use strict' import { describe, expect, test } from '@jest/globals' -import Store, { Optgroup, Option } from './store' +import Store, { DataArray, DataObjectPartial, Optgroup, Option } from './store' describe('store module', () => { - test('constructor', () => { - let store = new Store('single', []) - expect(store).toBeInstanceOf(Store) - }) - - test('set data', () => { - let store = new Store('single', [ - { - text: 'test', - }, - ]) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(1) - expect((data[0] as Option).text).toBe('test') - - // Make sure the data has all the default fields - expect((data[0] as Option).id).toBeDefined() - expect((data[0] as Option).value).toBeDefined() - expect((data[0] as Option).text).toBeDefined() - expect((data[0] as Option).html).toBeDefined() - expect((data[0] as Option).selected).toBeDefined() - expect((data[0] as Option).display).toBeDefined() - expect((data[0] as Option).disabled).toBeDefined() - expect((data[0] as Option).placeholder).toBeDefined() - expect((data[0] as Option).class).toBeDefined() - expect((data[0] as Option).style).toBeDefined() - expect((data[0] as Option).data).toBeDefined() - expect((data[0] as Option).mandatory).toBeDefined() - }) - - test('set data with optgroup', () => { - let store = new Store('single', [ - { - label: 'test', - options: [ - { - text: 'test', - }, - ], - }, - ]) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(1) - expect((data[0] as Optgroup).label).toBe('test') - expect((data[0] as Optgroup).options?.length).toBe(1) - expect((data[0] as Optgroup).options?.[0].text).toBe('test') - }) - - test('set data with optgroup and option', () => { - let store = new Store('single', [ - { - label: 'test', - options: [ - { - text: 'test', - }, - ], - }, - { - text: 'test', - }, - ]) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(2) - expect((data[0] as Optgroup).label).toBe('test') - expect((data[0] as Optgroup).options?.length).toBe(1) - expect((data[0] as Optgroup).options?.[0].text).toBe('test') - expect((data[1] as Option).text).toBe('test') - }) - - test('set data and set selected by ID', () => { - let store = new Store('single', [ - { - id: '8675309', - text: 'test', - }, - ]) - store.setSelectedBy('id', ['8675309']) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(1) - expect((data[0] as Option).text).toBe('test') - expect((data[0] as Option).selected).toBe(true) - }) - - test('set data and set selected by value', () => { - let store = new Store('single', [ - { - text: 'test', - value: 'hello', - }, - ]) - store.setSelectedBy('value', ['hello']) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(1) - expect((data[0] as Option).text).toBe('test') - expect((data[0] as Option).selected).toBe(true) - }) - - test('set data and set selected to empty string', () => { - let store = new Store('single', [ - { - text: 'all', - value: '', - }, - { - text: 'Value 1', - value: '1', - selected: true, - }, - ]) - store.setSelectedBy('value', ['']) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(2) - expect((data[0] as Option).text).toBe('all') - expect((data[0] as Option).selected).toBe(true) - }) - - test('set data and set selected by value multiple for single element', () => { - let store = new Store('single', [ - { - text: 'test1', - }, - { - text: 'test2', - }, - ]) - store.setSelectedBy('value', ['test1', 'test2']) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(2) - expect((data[0] as Option).text).toBe('test1') - expect((data[0] as Option).selected).toBe(true) - expect((data[1] as Option).text).toBe('test2') - expect((data[1] as Option).selected).toBe(false) - }) - - test('set data and search', () => { - let store = new Store('single', [ - { - text: 'test1', - }, - { - text: 'test2', - value: 'test2', - }, - { - text: 'test3', - }, - ]) - - let data = store.getData() - - const searchFilter = (opt: Option, search: string): boolean => { - return opt.text.toLowerCase().indexOf(search.toLowerCase()) !== -1 - } - - // With searchFilter search against current store data set - let search = store.search('test2', searchFilter) - expect(search.length).toBe(1) - expect((search[0] as Option).value).toBe('test2') - }) - - test('set data with non selected on single select', () => { - let store = new Store('single', [ - { - text: 'test1', - }, - { - text: 'test2', - value: 'test2', - }, - { - text: 'test3', - }, - ]) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(3) - expect((data[0] as Option).text).toBe('test1') - expect((data[0] as Option).selected).toBe(true) - expect((data[1] as Option).text).toBe('test2') - expect((data[1] as Option).selected).toBe(false) - expect((data[2] as Option).text).toBe('test3') - expect((data[2] as Option).selected).toBe(false) - }) - - test('set data with multiple selected on single select', () => { - let store = new Store('single', [ - { - text: 'test1', - }, - { - text: 'test2', - value: 'test2', - selected: true, - }, - { - text: 'test3', - selected: true, - }, - ]) - - let data = store.getData() - - // Make sure data has one item and that it has the correct text - expect(data.length).toBe(3) - expect((data[0] as Option).text).toBe('test1') - expect((data[0] as Option).selected).toBe(false) - expect((data[1] as Option).text).toBe('test2') - expect((data[1] as Option).selected).toBe(true) - expect((data[2] as Option).text).toBe('test3') - expect((data[2] as Option).selected).toBe(false) + describe('constructor', () => { + test('constructor without data', () => { + let store = new Store('single', []) + expect(store).toBeInstanceOf(Store) + }) + + test('constructor with single option', () => { + const store = new Store('single', [ + { + text: 'test', + }, + ]) + + const data = store.getData() + + // Make sure data has one item and that it has the correct text + expect(data).toHaveLength(1) + expect(data[0]).toBeInstanceOf(Option) + + const option = data[0] as Option + + expect(option.text).toBe('test') + expect(option.id).toBeDefined() + expect(option.value).toBeDefined() + expect(option.text).toBeDefined() + expect(option.html).toBeDefined() + expect(option.selected).toBeDefined() + expect(option.display).toBeDefined() + expect(option.disabled).toBeDefined() + expect(option.placeholder).toBeDefined() + expect(option.class).toBeDefined() + expect(option.style).toBeDefined() + expect(option.data).toBeDefined() + expect(option.mandatory).toBeDefined() + }) + + test('constructor with optgroup', () => { + const store = new Store('single', [ + { + label: 'opt group', + options: [ + { + text: 'test', + }, + ], + }, + ]) + + const data = store.getData() + + expect(data).toHaveLength(1) + + expect(data[0]).toBeInstanceOf(Optgroup) + const optGroup = data[0] as Optgroup + + expect(optGroup.label).toBe('opt group') + expect(optGroup.options).toHaveLength(1) + expect(optGroup.options?.[0].text).toBe('test') + }) + + test('constructor with optgroup and option', () => { + const store = new Store('single', [ + { + label: 'opt group', + options: [ + { + text: 'opt group option', + }, + ], + }, + { + text: 'option', + }, + ]) + + const data = store.getData() + + expect(data).toHaveLength(2) + + expect(data[0]).toBeInstanceOf(Optgroup) + const optGroup = data[0] as Optgroup + + expect(optGroup.label).toBe('opt group') + expect(optGroup.options).toHaveLength(1) + expect(optGroup.options?.[0].text).toBe('opt group option') + + expect(data[1]).toBeInstanceOf(Option) + expect((data[1] as Option).text).toBe('option') + }) + + test('constructor with multiple selected on single select only registers first selected', () => { + const store = new Store('single', [ + { + text: 'test1', + }, + { + text: 'test2', + value: 'test2', + selected: true, + }, + { + text: 'test3', + selected: true, + }, + ]) + + // cast to an option array here, so we don't need casts in the comparisons + const data = store.getData() as Array