| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import { fireEvent, render } from 'spec/helpers/testing-library'; |
| |
| import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown'; |
| import IconButton from 'src/dashboard/components/IconButton'; |
| import { DASHBOARD_GRID_ID } from 'src/dashboard/util/constants'; |
| |
| import { getMockStore } from 'spec/fixtures/mockStore'; |
| import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout'; |
| import { initialState } from 'src/SqlLab/fixtures'; |
| import Row from './Row'; |
| |
| jest.mock('@superset-ui/core', () => ({ |
| ...jest.requireActual('@superset-ui/core'), |
| isFeatureEnabled: jest.fn(() => true), |
| FeatureFlag: { |
| DashboardVirtualization: 'DASHBOARD_VIRTUALIZATION', |
| }, |
| })); |
| |
| jest.mock('src/utils/isBot', () => ({ |
| isCurrentUserBot: jest.fn(() => false), |
| })); |
| |
| jest.mock('src/dashboard/util/isEmbedded', () => ({ |
| isEmbedded: jest.fn(() => false), |
| })); |
| |
| jest.mock('src/dashboard/components/dnd/DragDroppable', () => ({ |
| Draggable: ({ children }) => ( |
| <div data-test="mock-draggable">{children({})}</div> |
| ), |
| Droppable: ({ children, depth }) => ( |
| <div data-test="mock-droppable" depth={depth}> |
| {children({})} |
| </div> |
| ), |
| })); |
| jest.mock( |
| 'src/dashboard/containers/DashboardComponent', |
| () => |
| ({ availableColumnCount, depth }) => ( |
| <div data-test="mock-dashboard-component" depth={depth}> |
| {availableColumnCount} |
| </div> |
| ), |
| ); |
| |
| jest.mock( |
| 'src/dashboard/components/menu/WithPopoverMenu', |
| () => |
| ({ children }) => <div data-test="mock-with-popover-menu">{children}</div>, |
| ); |
| |
| jest.mock( |
| 'src/dashboard/components/DeleteComponentButton', |
| () => |
| ({ onDelete }) => ( |
| <button |
| type="button" |
| data-test="mock-delete-component-button" |
| onClick={onDelete} |
| > |
| Delete |
| </button> |
| ), |
| ); |
| |
| const rowWithoutChildren = { ...mockLayout.present.ROW_ID, children: [] }; |
| const props = { |
| id: 'ROW_ID', |
| parentId: DASHBOARD_GRID_ID, |
| component: mockLayout.present.ROW_ID, |
| parentComponent: mockLayout.present[DASHBOARD_GRID_ID], |
| index: 0, |
| depth: 2, |
| editMode: false, |
| availableColumnCount: 12, |
| columnWidth: 50, |
| occupiedColumnCount: 6, |
| onResizeStart() {}, |
| onResize() {}, |
| onResizeStop() {}, |
| handleComponentDrop() {}, |
| deleteComponent() {}, |
| updateComponents() {}, |
| }; |
| |
| function setup(overrideProps) { |
| // We have to wrap provide DragDropContext for the underlying DragDroppable |
| // otherwise we cannot assert on DragDroppable children |
| const mockStore = getMockStore({ |
| ...initialState, |
| }); |
| |
| return render(<Row {...props} {...overrideProps} />, { |
| store: mockStore, |
| useDnd: true, |
| useRouter: true, |
| }); |
| } |
| |
| test('should render a Draggable', () => { |
| // don't count child DragDroppables |
| const { getByTestId, queryByTestId } = setup({ |
| component: rowWithoutChildren, |
| }); |
| |
| expect(getByTestId('mock-draggable')).toBeInTheDocument(); |
| expect(queryByTestId('mock-droppable')).not.toBeInTheDocument(); |
| }); |
| |
| test('should skip rendering HoverMenu and DeleteComponentButton when not in editMode', () => { |
| const { container, queryByTestId } = setup({ |
| component: rowWithoutChildren, |
| }); |
| expect(container.querySelector('.hover-menu')).not.toBeInTheDocument(); |
| expect(queryByTestId('mock-delete-component-button')).not.toBeInTheDocument(); |
| }); |
| |
| test('should render a WithPopoverMenu', () => { |
| // don't count child DragDroppables |
| const { getByTestId } = setup({ component: rowWithoutChildren }); |
| expect(getByTestId('mock-with-popover-menu')).toBeInTheDocument(); |
| }); |
| |
| test('should render a HoverMenu in editMode', () => { |
| const { container, getAllByTestId, getByTestId } = setup({ |
| component: rowWithoutChildren, |
| editMode: true, |
| }); |
| expect(container.querySelector('.hover-menu')).toBeInTheDocument(); |
| |
| // Droppable area enabled in editMode |
| expect(getAllByTestId('mock-droppable').length).toBe(1); |
| |
| // pass the same depth of its droppable area |
| expect(getByTestId('mock-droppable')).toHaveAttribute( |
| 'depth', |
| `${props.depth}`, |
| ); |
| }); |
| |
| test('should render a DeleteComponentButton in editMode', () => { |
| const { getByTestId } = setup({ |
| component: rowWithoutChildren, |
| editMode: true, |
| }); |
| expect(getByTestId('mock-delete-component-button')).toBeInTheDocument(); |
| }); |
| |
| test.skip('should render a BackgroundStyleDropdown when focused', () => { |
| let wrapper = setup({ component: rowWithoutChildren }); |
| expect(wrapper.find(BackgroundStyleDropdown)).toBeFalsy(); |
| |
| // we cannot set props on the Row because of the WithDragDropContext wrapper |
| wrapper = setup({ component: rowWithoutChildren, editMode: true }); |
| wrapper |
| .find(IconButton) |
| .at(1) // first one is delete button |
| .simulate('click'); |
| |
| expect(wrapper.find(BackgroundStyleDropdown)).toBeTruthy(); |
| }); |
| |
| test('should call deleteComponent when deleted', () => { |
| const deleteComponent = jest.fn(); |
| const { getByTestId } = setup({ editMode: true, deleteComponent }); |
| fireEvent.click(getByTestId('mock-delete-component-button')); |
| expect(deleteComponent).toHaveBeenCalledTimes(1); |
| }); |
| |
| test('should pass appropriate availableColumnCount to children', () => { |
| const { getByTestId } = setup(); |
| expect(getByTestId('mock-dashboard-component')).toHaveTextContent( |
| props.availableColumnCount - props.occupiedColumnCount, |
| ); |
| }); |
| |
| test('should increment the depth of its children', () => { |
| const { getByTestId } = setup(); |
| expect(getByTestId('mock-dashboard-component')).toHaveAttribute( |
| 'depth', |
| `${props.depth + 1}`, |
| ); |
| }); |
| |
| // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks |
| describe('visibility handling for intersection observers', () => { |
| const mockIntersectionObserver = jest.fn(); |
| const mockObserve = jest.fn(); |
| const mockDisconnect = jest.fn(); |
| |
| beforeAll(() => { |
| mockIntersectionObserver.mockReturnValue({ |
| observe: mockObserve, |
| unobserve: jest.fn(), |
| disconnect: mockDisconnect, |
| }); |
| window.IntersectionObserver = mockIntersectionObserver; |
| }); |
| |
| beforeEach(() => { |
| jest.clearAllMocks(); |
| }); |
| |
| afterAll(() => { |
| delete window.IntersectionObserver; |
| }); |
| |
| test('should handle visibility prop changes without crashing', () => { |
| const { rerender } = setup({ isComponentVisible: true }); |
| |
| expect(setup).not.toThrow(); |
| |
| expect(() => { |
| rerender(<Row {...props} isComponentVisible={false} />); |
| }).not.toThrow(); |
| |
| expect(() => { |
| rerender(<Row {...props} isComponentVisible />); |
| }).not.toThrow(); |
| }); |
| |
| test('should create intersection observers when feature is enabled', () => { |
| setup({ isComponentVisible: true }); |
| |
| expect(mockIntersectionObserver).toHaveBeenCalledWith( |
| expect.any(Function), |
| expect.objectContaining({ rootMargin: expect.any(String) }), |
| ); |
| }); |
| |
| test('should not create intersection observers when feature is disabled', () => { |
| const coreMock = jest.requireMock('@superset-ui/core'); |
| coreMock.isFeatureEnabled.mockReturnValue(false); |
| |
| jest.clearAllMocks(); |
| setup({ isComponentVisible: true }); |
| |
| expect(mockIntersectionObserver).not.toHaveBeenCalled(); |
| |
| coreMock.isFeatureEnabled.mockReturnValue(true); |
| }); |
| |
| test('intersection observer callbacks handle entries without errors', () => { |
| const callback = ([entry]) => { |
| if (entry.isIntersecting) return true; |
| |
| return false; |
| }; |
| |
| const intersectingEntry = { isIntersecting: true }; |
| expect(() => callback([intersectingEntry])).not.toThrow(); |
| expect(callback([intersectingEntry])).toBe(true); |
| |
| const nonIntersectingEntry = { isIntersecting: false }; |
| expect(() => callback([nonIntersectingEntry])).not.toThrow(); |
| expect(callback([nonIntersectingEntry])).toBe(false); |
| }); |
| }); |