import React from 'react'; import { act } from '@testing-library/react'; import { registerAllModules } from 'handsontable/registry'; import { HotTable } from '../src/hotTable'; import { createSpreadsheetData, mockElementDimensions, RendererComponent, EditorComponent, sleep, simulateKeyboardEvent, simulateMouseEvent, mountComponent, mountComponentWithRef, customNativeRenderer, CustomNativeEditor, renderHotTableWithProps } from './_helpers'; import { OBSOLETE_HOTEDITOR_WARNING, OBSOLETE_HOTRENDERER_WARNING, UNEXPECTED_HOTTABLE_CHILDREN_WARNING } from '../src/helpers' import { HotTableProps, HotTableRef } from '../src/types' import { HotColumn } from '../src/hotColumn' // register Handsontable's modules registerAllModules(); describe('Handsontable initialization', () => { it('should render Handsontable when using the HotTable component', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; expect(hotInstance as any).not.toBe(null); expect(hotInstance as any).not.toBe(void 0); expect(hotInstance.rootElement.id).toEqual('test-hot'); }); it('should pass the provided properties to the Handsontable instance', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; expect(hotInstance.rootElement.id).toBe('test-hot'); expect(hotInstance.getSettings().contextMenu).toBe(true); expect(hotInstance.getSettings().rowHeaders).toBe(true); expect(hotInstance.getSettings().colHeaders).toBe(true); expect(JSON.stringify(hotInstance.getData())).toEqual('[[2]]'); }); }); describe('Updating the Handsontable settings', () => { it('should call the updateSettings method of Handsontable, when the component properties get updated', async () => { const hotSettings: HotTableProps = { licenseKey: "non-commercial-and-evaluation", id: "hot", autoRowSize: false, autoColumnSize: false, }; const hotTableRef = renderHotTableWithProps(hotSettings, false); const hotInstance = hotTableRef.current!.hotInstance!; let updateSettingsCount = 0; hotInstance.addHook('afterUpdateSettings', () => { updateSettingsCount++; }); await sleep(300); act(() => { hotSettings.data = [[2]]; hotSettings.contextMenu = true; hotSettings.readOnly = true; renderHotTableWithProps(hotSettings, false, hotTableRef); }); expect(updateSettingsCount).toEqual(1); }); it('should update the Handsontable options, when the component properties get updated', async () => { const hotSettings: HotTableProps = { licenseKey: "non-commercial-and-evaluation", id: "hot", autoRowSize: false, autoColumnSize: false, }; const hotTableRef = renderHotTableWithProps(hotSettings, false); const hotInstance = hotTableRef.current!.hotInstance!; expect(hotInstance.getSettings().contextMenu).toEqual(void 0); expect(hotInstance.getSettings().readOnly).toEqual(false); expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null]]'); await sleep(300); act(() => { hotSettings.data = [[2]]; hotSettings.contextMenu = true; hotSettings.readOnly = true; renderHotTableWithProps(hotSettings, false, hotTableRef); }); expect(hotInstance.getSettings().contextMenu).toBe(true); expect(hotInstance.getSettings().readOnly).toBe(true); expect(JSON.stringify(hotInstance.getSettings().data)).toEqual('[[2]]'); }); it('should throw an error when trying to update init-only settings after inializing the component', async () => { console.error = jasmine.createSpy('error'); let updateState = null; function ExampleComponent() { const [renderAllRows, setRenderAllRows] = React.useState(false); (updateState as any) = setRenderAllRows; return ( <> ) } mountComponent(( )); const updateStateAndRender = () => act(() => (updateState as any)(true)); await expect(updateStateAndRender).toThrowError( 'The `renderAllRows` option can not be updated after the Handsontable is initialized.' ); expect(console.error).toHaveBeenCalled(); }); it('should NOT throw an error when trying to update settings after inializing the component if the other settings' + 'contain init-only entries', async () => { console.error = jasmine.createSpy('error'); let updateState = null; function ExampleComponent() { const [rowHeaders, setRowHeaders] = React.useState(false); (updateState as any) = setRowHeaders; return ( <> ) } mountComponent(( )); const updateStateAndRender = () => act(() => (updateState as any)(true)); await expect(updateStateAndRender).not.toThrowError(); expect(console.error).not.toHaveBeenCalled(); }); it('should NOT throw an error when definiting init-only settings, without updating them afterwards', async () => { console.error = jasmine.createSpy('error'); function ExampleComponent() { return ( <> ) } mountComponent(( )); expect(console.error).not.toHaveBeenCalled(); }); }); describe('Renderer configuration using React components', () => { it('should use the renderer component as Handsontable renderer, when it\'s passed as component to HotTable renderer prop', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; expect(hotInstance.getCell(0, 0)!.innerHTML).toEqual('
value: A1
'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 0, }); // For some reason it needs another render hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 1)!.innerHTML).toEqual('
value: B100
'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 99, }); hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 99)!.innerHTML).toEqual('
value: CV100
'); }); it('should use the renderer component as Handsontable renderer, when it\'s passed inline to HotTable renderer prop', async () => { const hotInstance = mountComponentWithRef(( }> )).hotInstance!; expect(hotInstance.getCell(0, 0)!.innerHTML).toEqual('
value: A1
'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 0, }); // For some reason it needs another render hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 1)!.innerHTML).toEqual('
value: B100
'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 99, }); hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 99)!.innerHTML).toEqual('
value: CV100
'); }); it('should use the renderer function as native Handsontable renderer, when it\'s passed to HotTable hotRenderer prop', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; expect(hotInstance.getCell(0, 0)!.innerHTML).toEqual('value: A1'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 0, }); // For some reason it needs another render hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 1)!.innerHTML).toEqual('value: B100'); await act(async() => { hotInstance.scrollViewportTo({ row: 99, col: 99, }); hotInstance.render(); }); await sleep(100); expect(hotInstance.getCell(99, 99)!.innerHTML).toEqual('value: CV100'); }); it('should issue a warning when the renderer component is nested under HotTable and assigned the \'hot-renderer\' attribute', async () => { console.warn = jasmine.createSpy('warn'); const hotInstance = mountComponentWithRef(( {/* @ts-ignore */} )).hotInstance!; expect(hotInstance.getCell(0, 0)!.innerHTML).not.toEqual('
value: A1
'); expect(console.warn).toHaveBeenCalledWith(OBSOLETE_HOTRENDERER_WARNING); }); }); describe('Editor configuration using React components', () => { it('should use the editor component as Handsontable editor and mount it in the root tree of the document', async () => { mountComponentWithRef(( )); const editorElement = document.querySelector('#editorComponentContainer')!; expect(editorElement.parentElement!.parentElement).toBe(document.body); }); it('should use the editor component as Handsontable editor, when it\'s passed as component to HotTable editor prop', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); await act(async () => { hotInstance.selectCell(0, 0); simulateKeyboardEvent('keydown', 13); }); expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('block'); expect(hotInstance.getDataAtCell(0, 0)).toEqual('A1'); await act(async () => { simulateMouseEvent(document.querySelector('#editorComponentContainer button'), 'click'); }); expect(hotInstance.getDataAtCell(0, 0)).toEqual('new-value'); await act(async () => { hotInstance.getActiveEditor()!.close(); }); expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); }); it('should use the editor component as Handsontable editor, when it\'s passed inline to HotTable editor prop', async () => { const hotInstance = mountComponentWithRef(( } /> )).hotInstance!; expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); await act(async () => { hotInstance.selectCell(0, 0); simulateKeyboardEvent('keydown', 13); }); expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('block'); expect(hotInstance.getDataAtCell(0, 0)).toEqual('A1'); await act(async () => { simulateMouseEvent(document.querySelector('#editorComponentContainer button'), 'click'); }); expect(hotInstance.getDataAtCell(0, 0)).toEqual('new-value'); await act(async () => { hotInstance.getActiveEditor()!.close(); }); expect((document.querySelector('#editorComponentContainer') as any).style.display).toEqual('none'); }); it('should use the editor class as native Handsontable editor, when it\'s passed to HotTable hotEditor prop', async () => { const hotInstance = mountComponentWithRef(( )).hotInstance!; await act(async () => { hotInstance.selectCell(0, 1); simulateKeyboardEvent('keydown', 13); (document.activeElement as HTMLInputElement).value = 'hello'; hotInstance.getActiveEditor()!.finishEditing(false); }); expect(hotInstance.getDataAtCell(0, 1)).toEqual('--hello--'); }); it('should use the correct editor inside HotTable component depends on its mount state', async () => { const hotTableInstanceRef = React.createRef(); const hotSettings: HotTableProps = { licenseKey: "non-commercial-and-evaluation", id: "test-hot", data: createSpreadsheetData(3, 3), width: 300, height: 300, rowHeights: 23, colWidths: 50, init: function () { mockElementDimensions(this.rootElement, 300, 300); }, }; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); const hotInstance = hotTableInstanceRef.current!.hotInstance!; await act(async() => { hotInstance.selectCell(0, 0); }); { const activeEditor = hotInstance.getActiveEditor()!; expect(activeEditor.constructor.name).toBe('TextEditor'); activeEditor.close(); } act(() => { hotSettings.editor = EditorComponent; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); }); await sleep(100); await act(async() => { hotInstance.selectCell(0, 0); }); { const activeEditor = hotInstance.getActiveEditor()!; expect(activeEditor.constructor.name).toBe('CustomEditor'); activeEditor.close(); } act(() => { hotSettings.editor = undefined; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); }); await sleep(100); await act(async() => { hotInstance.selectCell(0, 0); }); { const activeEditor = hotInstance.getActiveEditor()!; expect(activeEditor.constructor.name).toBe('TextEditor'); activeEditor.close(); } }); it('should use the correct renderer inside HotTable component depends on its mount state', async () => { const hotTableInstanceRef = React.createRef(); const hotSettings: HotTableProps = { licenseKey: "non-commercial-and-evaluation", id: "test-hot", width: 300, height: 300, rowHeights: 23, colWidths: 50, init: function () { mockElementDimensions(this.rootElement, 300, 300); }, }; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); const hotInstance = hotTableInstanceRef.current!.hotInstance!; await act(async() => { hotInstance.selectCell(0, 0); }); { const activeRenderer = hotInstance.getCellRenderer(0, 0); expect(activeRenderer.name).toBe('textRenderer'); } act(() => { hotSettings.renderer = RendererComponent; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); }); await sleep(100); await act(async() => { hotInstance.selectCell(0, 0); }); { const activeRenderer = hotInstance.getCellRenderer(0, 0); expect(activeRenderer.name).toBe('__internalRenderer'); } await act(async() => { hotSettings.renderer = undefined; renderHotTableWithProps(hotSettings, false, hotTableInstanceRef); }); await sleep(100); await act(async() => { hotInstance.selectCell(0, 0); }); { const activeRenderer = hotInstance.getCellRenderer(0, 0); expect(activeRenderer.name).toBe('textRenderer'); } }); it('should issue a warning when the editor component is nested under HotTable and assigned the \'hot-editor\' attribute', async () => { console.warn = jasmine.createSpy('warn'); mountComponentWithRef(( {/* @ts-ignore */} )); expect(document.querySelector('#editorComponentContainer')).not.toBeTruthy(); expect(console.warn).toHaveBeenCalledWith(OBSOLETE_HOTEDITOR_WARNING); expect(console.warn).not.toHaveBeenCalledWith(UNEXPECTED_HOTTABLE_CHILDREN_WARNING); }); }); describe('Passing children', () => { it('should not issue a warning when only HotColumn components are nested under HotTable', async () => { console.warn = jasmine.createSpy('warn'); mountComponentWithRef(( )); expect(console.warn).not.toHaveBeenCalledWith(UNEXPECTED_HOTTABLE_CHILDREN_WARNING); }); it('should issue a warning when the unknown component is nested under HotTable', async () => { console.warn = jasmine.createSpy('warn'); mountComponentWithRef((
Something unexpected
)); expect(console.warn).toHaveBeenCalledWith(UNEXPECTED_HOTTABLE_CHILDREN_WARNING); }); });