Repository: gravity-ui/uikit
Pull request #354 (2022-10-13 06:03:20)

test(Select): add tests for quick search

gravity-ui-bot: [Preview](https://preview.yandexcloud.dev/uikit/354/) is ready.
<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/jest.config.js b/jest.config.js
index 98eaac72..7effdf19 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -19,4 +19,5 @@ module.exports = {
     moduleNameMapper: {
         '\\.(css|less|scss|sass)$': 'jest-transform-css',
     },
+    testMatch: ['**/*.test.[jt]s?(x)'],
 };
diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx
index a27e28fe..1ae3e202 100644
--- a/src/components/Select/Select.tsx
+++ b/src/components/Select/Select.tsx
@@ -17,7 +17,7 @@ import {
 } from './utils';
 import {SelectControl, SelectPopup} from './components';
 import {Option, OptionGroup} from './tech-components';
-import {LIST_CLASSNAME} from './constants';
+import {LIST_CLASSNAME, QUICK_SEARCH_TIMEOUT} from './constants';
 
 type SelectComponent = React.ForwardRefExoticComponent<
     SelectProps & React.RefAttributes<HTMLButtonElement>
@@ -138,7 +138,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
             if (nextQuickSearch) {
                 const nextTimer = window.setTimeout(() => {
                     dispatch({type: 'SET_QUICK_SEARCH', payload: {quickSearch: ''}});
-                }, 2000);
+                }, QUICK_SEARCH_TIMEOUT);
                 dispatch({type: 'SET_QUICK_SEARCH_TIMER', payload: {quickSearchTimer: nextTimer}});
             }
         },
@@ -210,6 +210,8 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
         if (!active && typeof quickSearchTimer === 'number') {
             clearTimeout(quickSearchTimer);
         }
+
+        return () => clearTimeout(quickSearchTimer);
     }, [active, quickSearchTimer]);
 
     return (
diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts
index eedf941e..f0fd092f 100644
--- a/src/components/Select/constants.ts
+++ b/src/components/Select/constants.ts
@@ -18,6 +18,8 @@ export const BORDER_WIDTH = 1;
 
 export const POPUP_MIN_WIDTH_IN_VIRTUALIZE_CASE = 100;
 
+export const QUICK_SEARCH_TIMEOUT = 2000;
+
 export const SelectQa = {
     POPUP: 'select-popup',
 };


Test patch:
diff --git a/src/components/Select/__tests__/Select.base-actions.test.tsx b/src/components/Select/__tests__/Select.base-actions.test.tsx
index 80dd7f96..02c933b0 100644
--- a/src/components/Select/__tests__/Select.base-actions.test.tsx
+++ b/src/components/Select/__tests__/Select.base-actions.test.tsx
@@ -3,12 +3,19 @@ import userEvent from '@testing-library/user-event';
 import {render} from '@testing-library/react';
 import {ListQa} from '../../List';
 import {Select, SelectOption, SelectOptionGroup, SelectQa} from '..';
+import {QUICK_SEARCH_TIMEOUT} from '../constants';
+import {generateOptions, timeout} from './utils';
 
 const TEST_QA = 'select-test-qa';
-const DEFAULT_OPTIONS: SelectOption[] = [
-    {value: 'js', content: 'Java Script'},
-    {value: 'python', content: 'Python'},
-    {value: 'ruby', content: 'Ruby'},
+const DEFAULT_OPTIONS = generateOptions([
+    ['js', 'JavaScript'],
+    ['python', 'Python'],
+    ['ruby', 'Ruby'],
+]);
+const QUICK_SEARCH_OPTIONS = generateOptions(40);
+const GROUPED_QUICK_SEARCH_OPTIONS: SelectOptionGroup[] = [
+    {label: 'Group 1', options: generateOptions(40).slice(0, 20)},
+    {label: 'Group 2', options: generateOptions(40).slice(20)},
 ];
 const GROUPED_OPTIONS: SelectOptionGroup[] = [
     {label: 'Group 1', options: DEFAULT_OPTIONS.slice(0, 2)},
@@ -23,113 +30,195 @@ describe('Select base actions', () => {
         return utils;
     };
 
-    test('open popup by click', async () => {
-        const {getByTestId} = setup();
-        const user = userEvent.setup();
-        const selectControl = getByTestId(TEST_QA);
-        await user.click(selectControl);
-        getByTestId(SelectQa.POPUP);
+    describe('open popup by', () => {
+        test('click', async () => {
+            const {getByTestId} = setup();
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            await user.click(selectControl);
+            getByTestId(SelectQa.POPUP);
+        });
+
+        test('Enter', async () => {
+            const {getByTestId} = setup();
+            const user = userEvent.setup();
+            await user.keyboard('[Tab]');
+            await user.keyboard('[Enter]');
+            getByTestId(SelectQa.POPUP);
+        });
+
+        test('Space', async () => {
+            const {getByTestId} = setup();
+            const user = userEvent.setup();
+            await user.keyboard('[Tab]');
+            await user.keyboard('[Space]');
+            getByTestId(SelectQa.POPUP);
+        });
     });
 
-    test('open popup by Enter', async () => {
-        const {getByTestId} = setup();
-        const user = userEvent.setup();
-        await user.keyboard('[Tab]');
-        await user.keyboard('[Enter]');
-        getByTestId(SelectQa.POPUP);
-    });
-
-    test('open popup by Space', async () => {
-        const {getByTestId} = setup();
-        const user = userEvent.setup();
-        await user.keyboard('[Tab]');
-        await user.keyboard('[Space]');
-        getByTestId(SelectQa.POPUP);
-    });
-
-    test('navigate in flat list by ArrowDown', async () => {
-        const {getByTestId} = setup();
-        const user = userEvent.setup();
-        const selectControl = getByTestId(TEST_QA);
-        const optionLabels = DEFAULT_OPTIONS.map(({content}) => content).concat(
-            DEFAULT_OPTIONS[0].content,
-        );
-
-        await user.click(selectControl);
-
-        for await (const content of optionLabels) {
-            const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
-            expect(selectedItem.textContent).toBe(content);
-            await user.keyboard('[ArrowDown]');
-        }
-    });
-
-    test('navigate in flat list by ArrowUp', async () => {
-        const {getByTestId} = setup();
-        const user = userEvent.setup();
-        const selectControl = getByTestId(TEST_QA);
-        const optionLabels = DEFAULT_OPTIONS.map(({content}) => content).concat(
-            DEFAULT_OPTIONS[0].content,
-        );
-        optionLabels.reverse();
-
-        await user.click(selectControl);
-
-        for await (const content of optionLabels) {
-            const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
-            expect(selectedItem.textContent).toBe(content);
-            await user.keyboard('[ArrowUp]');
-        }
-    });
-
-    test('navigate in grouped list by ArrowDown', async () => {
-        const {getByTestId} = setup({options: GROUPED_OPTIONS});
-        const user = userEvent.setup();
-        const selectControl = getByTestId(TEST_QA);
-        const optionLabels = GROUPED_OPTIONS.reduce((acc, group, index) => {
-            if (group.options) {
-                acc.push(...group.options.map(({content}) => content));
+    describe('navigate in flat list by', () => {
+        test('ArrowDown', async () => {
+            const {getByTestId} = setup();
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            const optionLabels = DEFAULT_OPTIONS.map(({content}) => content).concat(
+                DEFAULT_OPTIONS[0].content,
+            );
+
+            await user.click(selectControl);
+
+            for await (const content of optionLabels) {
+                const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+                expect(selectedItem.textContent).toBe(content);
+                await user.keyboard('[ArrowDown]');
             }
-
-            if (GROUPED_OPTIONS[0].options && index === GROUPED_OPTIONS.length - 1) {
-                acc.push(GROUPED_OPTIONS[0].options[0].content);
+        });
+
+        test('ArrowUp', async () => {
+            const {getByTestId} = setup();
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            const optionLabels = DEFAULT_OPTIONS.map(({content}) => content).concat(
+                DEFAULT_OPTIONS[0].content,
+            );
+            optionLabels.reverse();
+
+            await user.click(selectControl);
+
+            for await (const content of optionLabels) {
+                const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+                expect(selectedItem.textContent).toBe(content);
+                await user.keyboard('[ArrowUp]');
             }
-
-            return acc;
-        }, [] as React.ReactNode[]);
-
-        await user.click(selectControl);
-
-        for await (const content of optionLabels) {
-            const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
-            expect(selectedItem.textContent).toBe(content);
-            await user.keyboard('[ArrowDown]');
-        }
+        });
     });
 
-    test('navigate in grouped list by ArrowUp', async () => {
-        const {getByTestId} = setup({options: GROUPED_OPTIONS});
-        const user = userEvent.setup();
-        const selectControl = getByTestId(TEST_QA);
-        const optionLabels = GROUPED_OPTIONS.reduce((acc, group, index) => {
-            if (group.options) {
-                acc.push(...group.options.map(({content}) => content));
+    describe('navigate in grouped list by', () => {
+        test('ArrowDown', async () => {
+            const {getByTestId} = setup({options: GROUPED_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            const optionLabels = GROUPED_OPTIONS.reduce((acc, group, index) => {
+                if (group.options) {
+                    acc.push(...group.options.map(({content}) => content));
+                }
+
+                if (GROUPED_OPTIONS[0].options && index === GROUPED_OPTIONS.length - 1) {
+                    acc.push(GROUPED_OPTIONS[0].options[0].content);
+                }
+
+                return acc;
+            }, [] as React.ReactNode[]);
+
+            await user.click(selectControl);
+
+            for await (const content of optionLabels) {
+                const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+                expect(selectedItem.textContent).toBe(content);
+                await user.keyboard('[ArrowDown]');
             }
-
-            if (GROUPED_OPTIONS[0].options && index === GROUPED_OPTIONS.length - 1) {
-                acc.push(GROUPED_OPTIONS[0].options[0].content);
+        });
+
+        test('ArrowUp', async () => {
+            const {getByTestId} = setup({options: GROUPED_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            const optionLabels = GROUPED_OPTIONS.reduce((acc, group, index) => {
+                if (group.options) {
+                    acc.push(...group.options.map(({content}) => content));
+                }
+
+                if (GROUPED_OPTIONS[0].options && index === GROUPED_OPTIONS.length - 1) {
+                    acc.push(GROUPED_OPTIONS[0].options[0].content);
+                }
+
+                return acc;
+            }, [] as React.ReactNode[]);
+            optionLabels.reverse();
+
+            await user.click(selectControl);
+
+            for await (const content of optionLabels) {
+                const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+                expect(selectedItem.textContent).toBe(content);
+                await user.keyboard('[ArrowUp]');
             }
+        });
+    });
 
-            return acc;
-        }, [] as React.ReactNode[]);
-        optionLabels.reverse();
-
-        await user.click(selectControl);
+    describe('find elements in flat list by "quick search"', () => {
+        test('with instant input', async () => {
+            const {getByTestId} = setup({options: QUICK_SEARCH_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            await user.click(selectControl);
+
+            await user.keyboard('3');
+            let selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+
+            await user.keyboard('5');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 35');
+
+            await user.keyboard('[Backspace]');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+        });
+
+        test('with delayed input', async () => {
+            const {getByTestId} = setup({options: QUICK_SEARCH_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            await user.click(selectControl);
+
+            await user.keyboard('3');
+            let selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+
+            await timeout(QUICK_SEARCH_TIMEOUT);
+
+            await user.keyboard('5');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 5');
+        });
+    });
 
-        for await (const content of optionLabels) {
-            const selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
-            expect(selectedItem.textContent).toBe(content);
-            await user.keyboard('[ArrowUp]');
-        }
+    describe('find elements in grouped list by "quick search"', () => {
+        test('with instant input', async () => {
+            const {getByTestId} = setup({options: GROUPED_QUICK_SEARCH_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            await user.click(selectControl);
+
+            await user.keyboard('3');
+            let selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+
+            await user.keyboard('5');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 35');
+
+            await user.keyboard('[Backspace]');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+        });
+
+        test('with delayed input', async () => {
+            const {getByTestId} = setup({options: GROUPED_QUICK_SEARCH_OPTIONS});
+            const user = userEvent.setup();
+            const selectControl = getByTestId(TEST_QA);
+            await user.click(selectControl);
+
+            await user.keyboard('3');
+            let selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 3');
+
+            await timeout(QUICK_SEARCH_TIMEOUT);
+
+            await user.keyboard('5');
+            selectedItem = getByTestId(ListQa.ACTIVE_ITEM);
+            expect(selectedItem.textContent).toBe('Value 5');
+        });
     });
 });
diff --git a/src/components/Select/__tests__/utils.ts b/src/components/Select/__tests__/utils.ts
new file mode 100644
index 00000000..986d1f84
--- /dev/null
+++ b/src/components/Select/__tests__/utils.ts
@@ -0,0 +1,24 @@
+import {act} from '@testing-library/react';
+import {range} from 'lodash';
+import {SelectOption} from '..';
+
+export const generateOptions = (args: number | [string, string][]): SelectOption[] => {
+    if (typeof args === 'number') {
+        return range(0, args).map((i) => ({
+            value: `val${i + 1}`,
+            content: `Value ${i + 1}`,
+        }));
+    }
+
+    return args.map(([value, content]) => ({value, content}));
+};
+
+export const timeout = (ms: number) => {
+    // https://testing-library.com/docs/react-testing-library/api/#act
+    // https://reactjs.org/docs/test-utils.html#act
+    return act(async () => {
+        return new Promise((resolve) => {
+            setTimeout(resolve, ms);
+        });
+    });
+};