blob: 253a41d0a10ea321ea393c8625aedcb4e6dd0717 [file] [log] [blame]
/**
* 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);
});
});