From 5a48d345b6d28a42ab8b9931f565cd65eb2e3ff2 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Sep 2024 17:12:46 +0300 Subject: [PATCH 1/4] [PickerInput]: added tests for value synchronization. --- .../src/rendering/setupComponentUtils.tsx | 9 + .../pickers/__tests__/PickerInput.test.tsx | 196 +++++++++++++++++- 2 files changed, 204 insertions(+), 1 deletion(-) diff --git a/test-utils/src/rendering/setupComponentUtils.tsx b/test-utils/src/rendering/setupComponentUtils.tsx index af65826a7b..65c60c4817 100644 --- a/test-utils/src/rendering/setupComponentUtils.tsx +++ b/test-utils/src/rendering/setupComponentUtils.tsx @@ -24,6 +24,7 @@ type PropsSubsetMock = { [key in keyof TProps]?: TMockFn }; type SetupComponentForTestReturnType = Promise<{ result: Awaited>, setProps: (propsToUpdate: PropsSubset) => void, + setPropsAsync: (propsToUpdate: PropsSubset) => Promise, mocks: PropsSubsetMock, }>; @@ -93,6 +94,14 @@ export async function setupComponentForTest, TMo }); }); }, + setPropsAsync: async (propsToUpdate: PropsSubset) => { + const propsToUpdateNames = Object.keys(propsToUpdate); + await act(async () => { + propsToUpdateNames.forEach((name) => { + propsContextRef.current?.setProperty(name as keyof TProps, propsToUpdate[name as keyof TProps]); + }); + }); + }, mocks, }; } diff --git a/uui/components/pickers/__tests__/PickerInput.test.tsx b/uui/components/pickers/__tests__/PickerInput.test.tsx index 13a587834b..5ad29240e2 100644 --- a/uui/components/pickers/__tests__/PickerInput.test.tsx +++ b/uui/components/pickers/__tests__/PickerInput.test.tsx @@ -13,7 +13,7 @@ import { Item, TestItemType, TestTreeItem, mockDataSource, mockDataSourceAsync, type PickerInputComponentProps = PickerInputProps; async function setupPickerInputForTest(params: Partial>) { - const { result, mocks, setProps } = await setupComponentForTest>( + const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( (context): PickerInputComponentProps => { if (params.selectionMode === 'single') { return Object.assign({ @@ -62,6 +62,7 @@ async function setupPickerInputForTest(param return { setProps, + setPropsAsync, result, mocks, dom: { input, container: result.container, target: result.container.firstElementChild as HTMLElement }, @@ -172,6 +173,48 @@ describe('PickerInput', () => { expect(screen.queryByText('C2')).not.toBeInTheDocument(); }); + it('[valueType id] should listen to value change', async () => { + let updatesCounter = 0; + const { dom, mocks, setProps } = await setupPickerInputForTest({ + onValueChange: jest.fn().mockImplementation((newValue) => { + if (updatesCounter === 0) { + setProps({ value: undefined }); + } else { + setProps({ value: newValue }); + } + updatesCounter++; + }), + selectionMode: 'single', + }); + expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + const optionC2 = await screen.findByText('C2'); + fireEvent.click(optionC2); + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith(12); + }); + + fireEvent.click(window.document.body); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByText('C2')).not.toBeInTheDocument(); + }); + + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + const option2C2 = await screen.findByText('C2'); + fireEvent.click(option2C2); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith(12); + }); + await waitFor(() => { + expect(screen.getByPlaceholderText('C2')).toBeInTheDocument(); + }); + }); + it('should close body on click outside', async () => { const { dom } = await setupPickerInputForTest({ value: undefined, @@ -251,6 +294,54 @@ describe('PickerInput', () => { }); }); + it('[valueType entity] should listen to value change', async () => { + let updatesCounter = 0; + let setPropsChain = Promise.resolve(); + const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + value: undefined, + onValueChange: jest.fn().mockImplementation((newValue) => { + setPropsChain = setPropsChain.then(() => { + if (updatesCounter === 0) { + updatesCounter++; + return setPropsAsync({ value: undefined }); + } else { + updatesCounter++; + return setPropsAsync({ value: newValue }); + } + }); + }), + selectionMode: 'single', + valueType: 'entity', + }); + expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + const optionC2 = await screen.findByText('C2'); + fireEvent.click(optionC2); + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith({ id: 12, level: 'C2', name: 'Proficiency' }); + }); + + fireEvent.click(window.document.body); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByText('C2')).not.toBeInTheDocument(); + }); + + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + const option2C2 = await screen.findByText('C2'); + fireEvent.click(option2C2); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith({ id: 12, level: 'C2', name: 'Proficiency' }); + }); + await waitFor(() => { + expect(screen.getByPlaceholderText('C2')).toBeInTheDocument(); + }); + }); + it('should render names of items by getName', async () => { const { mocks, dom } = await setupPickerInputForTest({ value: 3, @@ -534,6 +625,109 @@ describe('PickerInput', () => { expect(await PickerInputTestObject.getSelectedTagsText(dom.target)).toEqual(['A1', 'A1+']); }); + it('[valueType id] should listen to value change', async () => { + let updatesCounter = 0; + let setPropsChain = Promise.resolve(); + const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + value: undefined, + onValueChange: jest.fn().mockImplementation((newValue) => { + setPropsChain = setPropsChain.then(() => { + if (updatesCounter === 0) { + updatesCounter++; + return setPropsAsync({ value: [4] }); + } else { + updatesCounter++; + return setPropsAsync({ value: newValue }); + } + }); + }), + selectionMode: 'multi', + valueType: 'id', + }); + + expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + await PickerInputTestObject.clickOptionCheckbox('A1'); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith([2]); + }); + expect(await PickerInputTestObject.getSelectedTagsText(dom.target)).toEqual(['A2']); + + fireEvent.click(window.document.body); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await PickerInputTestObject.clickOptionCheckbox('A1'); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith([4, 2]); + }); + expect(await PickerInputTestObject.getSelectedTagsText(dom.target)).toEqual(['A2', 'A1']); + }); + + it('[valueType entity] should listen to value change', async () => { + let updatesCounter = 0; + let setPropsChain = Promise.resolve(); + const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + value: undefined, + onValueChange: jest.fn().mockImplementation((newValue) => { + setPropsChain = setPropsChain.then(() => { + if (updatesCounter === 0) { + updatesCounter++; + return setPropsAsync({ value: [{ id: 4, level: 'A2', name: 'Pre-Intermediate' }] }); + } else { + updatesCounter++; + return setPropsAsync({ value: newValue }); + } + }); + }), + selectionMode: 'multi', + valueType: 'entity', + }); + + expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + await PickerInputTestObject.clickOptionCheckbox('A1'); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith([{ + id: 2, + level: 'A1', + name: 'Elementary', + }]); + }); + expect(await PickerInputTestObject.getSelectedTagsText(dom.target)).toEqual(['A2']); + + fireEvent.click(window.document.body); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + fireEvent.click(dom.input); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + await act(async () => { + await PickerInputTestObject.clickOptionCheckbox('A1'); + }); + + await waitFor(() => { + expect(mocks.onValueChange).toHaveBeenLastCalledWith([{ + id: 4, + level: 'A2', + name: 'Pre-Intermediate', + }, + { + id: 2, + level: 'A1', + name: 'Elementary', + }]); + }); + expect(await PickerInputTestObject.getSelectedTagsText(dom.target)).toEqual(['A2', 'A1']); + }); + it('[valueType entity] should select & clear several options', async () => { const { dom, mocks } = await setupPickerInputForTest({ value: undefined, From 2e1ce1a048499bf67eee2f393ed6dfb094245083 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 11 Sep 2024 12:50:44 +0300 Subject: [PATCH 2/4] [PickerInput]: refactored previous solution. --- .../pickers/__tests__/PickerInput.test.tsx | 78 ++++++------------- 1 file changed, 24 insertions(+), 54 deletions(-) diff --git a/uui/components/pickers/__tests__/PickerInput.test.tsx b/uui/components/pickers/__tests__/PickerInput.test.tsx index 5ad29240e2..36a21f050d 100644 --- a/uui/components/pickers/__tests__/PickerInput.test.tsx +++ b/uui/components/pickers/__tests__/PickerInput.test.tsx @@ -10,14 +10,20 @@ import { PickerInput, PickerInputProps } from '../PickerInput'; import { IHasEditMode } from '../../types'; import { Item, TestItemType, TestTreeItem, mockDataSource, mockDataSourceAsync, mockSmallDataSourceAsync, mockTreeLikeDataSourceAsync } from './mocks'; -type PickerInputComponentProps = PickerInputProps; +type PickerInputComponentProps = PickerInputProps & { firstUpdateValue: TItem | TId | TItem[] | TId[], rewriteFirstUpdate?: boolean; }; async function setupPickerInputForTest(params: Partial>) { const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( (context): PickerInputComponentProps => { if (params.selectionMode === 'single') { + let updatesCounter = 0; return Object.assign({ onValueChange: jest.fn().mockImplementation((newValue) => { + if (params.rewriteFirstUpdate && updatesCounter === 0) { + updatesCounter++; + return context.current?.setProperty('value', params.firstUpdateValue); + } + if (typeof newValue === 'function') { const v = newValue(params.value); context.current?.setProperty('value', v); @@ -33,8 +39,14 @@ async function setupPickerInputForTest(param }, params) as PickerInputComponentProps; } + let updatesCounter = 0; return Object.assign({ onValueChange: jest.fn().mockImplementation((newValue) => { + if (params.rewriteFirstUpdate && updatesCounter === 0) { + updatesCounter++; + return context.current?.setProperty('value', params.firstUpdateValue); + } + if (typeof newValue === 'function') { const v = newValue(params.value); context.current?.setProperty('value', v); @@ -174,16 +186,8 @@ describe('PickerInput', () => { }); it('[valueType id] should listen to value change', async () => { - let updatesCounter = 0; - const { dom, mocks, setProps } = await setupPickerInputForTest({ - onValueChange: jest.fn().mockImplementation((newValue) => { - if (updatesCounter === 0) { - setProps({ value: undefined }); - } else { - setProps({ value: newValue }); - } - updatesCounter++; - }), + const { dom, mocks } = await setupPickerInputForTest({ + rewriteFirstUpdate: true, selectionMode: 'single', }); expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); @@ -295,21 +299,9 @@ describe('PickerInput', () => { }); it('[valueType entity] should listen to value change', async () => { - let updatesCounter = 0; - let setPropsChain = Promise.resolve(); - const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + const { dom, mocks } = await setupPickerInputForTest({ value: undefined, - onValueChange: jest.fn().mockImplementation((newValue) => { - setPropsChain = setPropsChain.then(() => { - if (updatesCounter === 0) { - updatesCounter++; - return setPropsAsync({ value: undefined }); - } else { - updatesCounter++; - return setPropsAsync({ value: newValue }); - } - }); - }), + rewriteFirstUpdate: true, selectionMode: 'single', valueType: 'entity', }); @@ -626,21 +618,10 @@ describe('PickerInput', () => { }); it('[valueType id] should listen to value change', async () => { - let updatesCounter = 0; - let setPropsChain = Promise.resolve(); - const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + const { dom, mocks } = await setupPickerInputForTest({ + rewriteFirstUpdate: true, + firstUpdateValue: [4], value: undefined, - onValueChange: jest.fn().mockImplementation((newValue) => { - setPropsChain = setPropsChain.then(() => { - if (updatesCounter === 0) { - updatesCounter++; - return setPropsAsync({ value: [4] }); - } else { - updatesCounter++; - return setPropsAsync({ value: newValue }); - } - }); - }), selectionMode: 'multi', valueType: 'id', }); @@ -670,21 +651,10 @@ describe('PickerInput', () => { }); it('[valueType entity] should listen to value change', async () => { - let updatesCounter = 0; - let setPropsChain = Promise.resolve(); - const { dom, mocks, setPropsAsync } = await setupPickerInputForTest({ + const { dom, mocks } = await setupPickerInputForTest({ + rewriteFirstUpdate: true, + firstUpdateValue: [{ id: 4, level: 'A2', name: 'Pre-Intermediate' }], value: undefined, - onValueChange: jest.fn().mockImplementation((newValue) => { - setPropsChain = setPropsChain.then(() => { - if (updatesCounter === 0) { - updatesCounter++; - return setPropsAsync({ value: [{ id: 4, level: 'A2', name: 'Pre-Intermediate' }] }); - } else { - updatesCounter++; - return setPropsAsync({ value: newValue }); - } - }); - }), selectionMode: 'multi', valueType: 'entity', }); @@ -1623,7 +1593,7 @@ describe('PickerInput', () => { ('should not call onValueChange on edit search with emptyValue = %s; and return emptyValue = %s on check -> uncheck', async (emptyValue) => { const { dom, mocks } = await setupPickerInputForTest({ emptyValue: emptyValue, - value: emptyValue, + value: emptyValue as (undefined | []), selectionMode: 'multi', searchPosition: 'body', }); From c50f7368f64d4165436a8f10008427f4f25b1364 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 11 Sep 2024 13:40:11 +0300 Subject: [PATCH 3/4] [PickerInput]: refactored tests. --- .../pickers/__tests__/PickerInput.test.tsx | 85 +++++++++++++++---- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/uui/components/pickers/__tests__/PickerInput.test.tsx b/uui/components/pickers/__tests__/PickerInput.test.tsx index 36a21f050d..48d2a87eda 100644 --- a/uui/components/pickers/__tests__/PickerInput.test.tsx +++ b/uui/components/pickers/__tests__/PickerInput.test.tsx @@ -10,18 +10,77 @@ import { PickerInput, PickerInputProps } from '../PickerInput'; import { IHasEditMode } from '../../types'; import { Item, TestItemType, TestTreeItem, mockDataSource, mockDataSourceAsync, mockSmallDataSourceAsync, mockTreeLikeDataSourceAsync } from './mocks'; -type PickerInputComponentProps = PickerInputProps & { firstUpdateValue: TItem | TId | TItem[] | TId[], rewriteFirstUpdate?: boolean; }; +type PickerInputComponentProps = PickerInputProps; async function setupPickerInputForTest(params: Partial>) { + const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( + (context): PickerInputComponentProps => { + if (params.selectionMode === 'single') { + return Object.assign({ + onValueChange: jest.fn().mockImplementation((newValue) => { + if (typeof newValue === 'function') { + const v = newValue(params.value); + context.current?.setProperty('value', v); + } + context.current?.setProperty('value', newValue); + }), + dataSource: mockDataSourceAsync, + disableClear: false, + searchPosition: 'input', + getName: (item: TestItemType) => item.level, + value: params.value as TId, + searchDebounceDelay: 0, + }, params) as PickerInputComponentProps; + } + + return Object.assign({ + onValueChange: jest.fn().mockImplementation((newValue) => { + if (typeof newValue === 'function') { + const v = newValue(params.value); + context.current?.setProperty('value', v); + return; + } + context.current?.setProperty('value', newValue); + }), + dataSource: mockDataSourceAsync, + disableClear: false, + searchPosition: 'input', + getName: (item: TestItemType) => item.level, + value: params.value as number[], + selectionMode: 'multi', + searchDebounceDelay: 0, + }, params) as PickerInputComponentProps; + }, + (props) => ( + <> + + + + ), + ); + const input = screen.queryByRole('textbox') as HTMLElement; + + return { + setProps, + setPropsAsync, + result, + mocks, + dom: { input, container: result.container, target: result.container.firstElementChild as HTMLElement }, + }; +} + +async function setupPickerInputForTestWithFirstValueChangeRewriting( + params: Partial & { valueForFirstUpdate: TItem | TId | TItem[] | TId[] }>, +) { const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( (context): PickerInputComponentProps => { if (params.selectionMode === 'single') { let updatesCounter = 0; return Object.assign({ onValueChange: jest.fn().mockImplementation((newValue) => { - if (params.rewriteFirstUpdate && updatesCounter === 0) { + if (updatesCounter === 0) { updatesCounter++; - return context.current?.setProperty('value', params.firstUpdateValue); + return context.current?.setProperty('value', params.valueForFirstUpdate); } if (typeof newValue === 'function') { @@ -42,9 +101,9 @@ async function setupPickerInputForTest(param let updatesCounter = 0; return Object.assign({ onValueChange: jest.fn().mockImplementation((newValue) => { - if (params.rewriteFirstUpdate && updatesCounter === 0) { + if (updatesCounter === 0) { updatesCounter++; - return context.current?.setProperty('value', params.firstUpdateValue); + return context.current?.setProperty('value', params.valueForFirstUpdate); } if (typeof newValue === 'function') { @@ -186,8 +245,7 @@ describe('PickerInput', () => { }); it('[valueType id] should listen to value change', async () => { - const { dom, mocks } = await setupPickerInputForTest({ - rewriteFirstUpdate: true, + const { dom, mocks } = await setupPickerInputForTestWithFirstValueChangeRewriting({ selectionMode: 'single', }); expect(PickerInputTestObject.getPlaceholderText(dom.input)).toEqual('Please select'); @@ -299,9 +357,8 @@ describe('PickerInput', () => { }); it('[valueType entity] should listen to value change', async () => { - const { dom, mocks } = await setupPickerInputForTest({ + const { dom, mocks } = await setupPickerInputForTestWithFirstValueChangeRewriting({ value: undefined, - rewriteFirstUpdate: true, selectionMode: 'single', valueType: 'entity', }); @@ -618,9 +675,8 @@ describe('PickerInput', () => { }); it('[valueType id] should listen to value change', async () => { - const { dom, mocks } = await setupPickerInputForTest({ - rewriteFirstUpdate: true, - firstUpdateValue: [4], + const { dom, mocks } = await setupPickerInputForTestWithFirstValueChangeRewriting({ + valueForFirstUpdate: [4], value: undefined, selectionMode: 'multi', valueType: 'id', @@ -651,9 +707,8 @@ describe('PickerInput', () => { }); it('[valueType entity] should listen to value change', async () => { - const { dom, mocks } = await setupPickerInputForTest({ - rewriteFirstUpdate: true, - firstUpdateValue: [{ id: 4, level: 'A2', name: 'Pre-Intermediate' }], + const { dom, mocks } = await setupPickerInputForTestWithFirstValueChangeRewriting({ + valueForFirstUpdate: [{ id: 4, level: 'A2', name: 'Pre-Intermediate' }], value: undefined, selectionMode: 'multi', valueType: 'entity', From 562d71b5c1d983f6977641d1e4cc26b69b608ef4 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Wed, 11 Sep 2024 13:43:35 +0300 Subject: [PATCH 4/4] [setupComponentUtils]: removed not used setPropsAsync. --- test-utils/src/rendering/setupComponentUtils.tsx | 9 --------- uui/components/pickers/__tests__/PickerInput.test.tsx | 6 ++---- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/test-utils/src/rendering/setupComponentUtils.tsx b/test-utils/src/rendering/setupComponentUtils.tsx index 65c60c4817..af65826a7b 100644 --- a/test-utils/src/rendering/setupComponentUtils.tsx +++ b/test-utils/src/rendering/setupComponentUtils.tsx @@ -24,7 +24,6 @@ type PropsSubsetMock = { [key in keyof TProps]?: TMockFn }; type SetupComponentForTestReturnType = Promise<{ result: Awaited>, setProps: (propsToUpdate: PropsSubset) => void, - setPropsAsync: (propsToUpdate: PropsSubset) => Promise, mocks: PropsSubsetMock, }>; @@ -94,14 +93,6 @@ export async function setupComponentForTest, TMo }); }); }, - setPropsAsync: async (propsToUpdate: PropsSubset) => { - const propsToUpdateNames = Object.keys(propsToUpdate); - await act(async () => { - propsToUpdateNames.forEach((name) => { - propsContextRef.current?.setProperty(name as keyof TProps, propsToUpdate[name as keyof TProps]); - }); - }); - }, mocks, }; } diff --git a/uui/components/pickers/__tests__/PickerInput.test.tsx b/uui/components/pickers/__tests__/PickerInput.test.tsx index 48d2a87eda..0199924601 100644 --- a/uui/components/pickers/__tests__/PickerInput.test.tsx +++ b/uui/components/pickers/__tests__/PickerInput.test.tsx @@ -13,7 +13,7 @@ import { Item, TestItemType, TestTreeItem, mockDataSource, mockDataSourceAsync, type PickerInputComponentProps = PickerInputProps; async function setupPickerInputForTest(params: Partial>) { - const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( + const { result, mocks, setProps } = await setupComponentForTest>( (context): PickerInputComponentProps => { if (params.selectionMode === 'single') { return Object.assign({ @@ -62,7 +62,6 @@ async function setupPickerInputForTest(param return { setProps, - setPropsAsync, result, mocks, dom: { input, container: result.container, target: result.container.firstElementChild as HTMLElement }, @@ -72,7 +71,7 @@ async function setupPickerInputForTest(param async function setupPickerInputForTestWithFirstValueChangeRewriting( params: Partial & { valueForFirstUpdate: TItem | TId | TItem[] | TId[] }>, ) { - const { result, mocks, setProps, setPropsAsync } = await setupComponentForTest>( + const { result, mocks, setProps } = await setupComponentForTest>( (context): PickerInputComponentProps => { if (params.selectionMode === 'single') { let updatesCounter = 0; @@ -133,7 +132,6 @@ async function setupPickerInputForTestWithFirstValueChangeRewriting