airbnb/caravel

View on GitHub
superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.test.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * 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 { useState } from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen, within } from 'spec/helpers/testing-library';
import setupPlugins from 'src/setup/setupPlugins';
import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { BinaryQueryObjectFilterClause } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import DrillDetailMenuItems, {
  DrillDetailMenuItemsProps,
} from './DrillDetailMenuItems';

/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */

jest.mock(
  './DrillDetailPane',
  () =>
    ({
      initialFilters,
    }: {
      initialFilters: BinaryQueryObjectFilterClause[];
    }) => <pre data-test="modal-filters">{JSON.stringify(initialFilters)}</pre>,
);

const { id: defaultChartId, form_data: defaultFormData } =
  chartQueries[sliceId];

const { slice_name: chartName } = defaultFormData;
const unsupportedChartFormData = {
  ...defaultFormData,
  viz_type: 'dist_bar',
};

const noDimensionsFormData = {
  ...defaultFormData,
  viz_type: 'table',
  query_mode: 'raw',
};

const filterA: BinaryQueryObjectFilterClause = {
  col: 'sample_column',
  op: '==',
  val: 1234567890,
  formattedVal: 'Yesterday',
};

const filterB: BinaryQueryObjectFilterClause = {
  col: 'sample_column_2',
  op: '==',
  val: 987654321,
  formattedVal: 'Two days ago',
};

const MockRenderChart = ({
  chartId,
  formData,
  isContextMenu,
  filters,
}: Partial<DrillDetailMenuItemsProps>) => {
  const [showMenu, setShowMenu] = useState(false);

  return (
    <Menu>
      <DrillDetailMenuItems
        chartId={chartId ?? defaultChartId}
        formData={formData ?? defaultFormData}
        filters={filters}
        isContextMenu={isContextMenu}
        showModal={showMenu}
        setShowModal={setShowMenu}
      />
    </Menu>
  );
};

const renderMenu = ({
  chartId,
  formData,
  isContextMenu,
  filters,
}: Partial<DrillDetailMenuItemsProps>) => {
  const store = getMockStoreWithNativeFilters();
  return render(
    <MockRenderChart
      chartId={chartId}
      formData={formData}
      isContextMenu={isContextMenu}
      filters={filters}
    />,
    { useRouter: true, useRedux: true, store },
  );
};

/**
 * Drill to Detail modal should appear with correct initial filters
 */
const expectDrillToDetailModal = async (
  buttonName: string,
  filters: BinaryQueryObjectFilterClause[] = [],
) => {
  const button = screen.getByRole('menuitem', { name: buttonName });
  userEvent.click(button);
  const modal = await screen.findByRole('dialog', {
    name: `Drill to detail: ${chartName}`,
  });

  expect(modal).toBeVisible();
  expect(screen.getByTestId('modal-filters')).toHaveTextContent(
    JSON.stringify(filters),
  );
};

/**
 * Menu item should be enabled without explanatory tooltip
 */
const expectMenuItemEnabled = async (menuItem: HTMLElement) => {
  expect(menuItem).toBeInTheDocument();
  expect(menuItem).not.toHaveAttribute('aria-disabled');
  const tooltipTrigger = within(menuItem).queryByTestId('tooltip-trigger');
  expect(tooltipTrigger).not.toBeInTheDocument();
};

/**
 * Menu item should be disabled, optionally with an explanatory tooltip
 */
const expectMenuItemDisabled = async (
  menuItem: HTMLElement,
  tooltipContent?: string,
) => {
  expect(menuItem).toBeVisible();
  expect(menuItem).toHaveAttribute('aria-disabled', 'true');
  const tooltipTrigger = within(menuItem).queryByTestId('tooltip-trigger');
  if (tooltipContent) {
    userEvent.hover(tooltipTrigger as HTMLElement);
    const tooltip = await screen.findByRole('tooltip', {
      name: tooltipContent,
    });

    expect(tooltip).toBeInTheDocument();
  } else {
    expect(tooltipTrigger).not.toBeInTheDocument();
  }
};

/**
 * "Drill to detail" item should be enabled and open the correct modal
 */
const expectDrillToDetailEnabled = async () => {
  const drillToDetailMenuItem = screen.getByRole('menuitem', {
    name: 'Drill to detail',
  });

  await expectMenuItemEnabled(drillToDetailMenuItem);
  await expectDrillToDetailModal('Drill to detail');
};

/**
 * "Drill to detail" item should be present and disabled
 */
const expectDrillToDetailDisabled = async (tooltipContent?: string) => {
  const drillToDetailMenuItem = screen.getByRole('menuitem', {
    name: 'Drill to detail',
  });

  await expectMenuItemDisabled(drillToDetailMenuItem, tooltipContent);
};

/**
 * "Drill to detail by" item should not be present
 */
const expectNoDrillToDetailBy = async () => {
  const drillToDetailBy = screen.queryByRole('menuitem', {
    name: 'Drill to detail by',
  });

  expect(drillToDetailBy).not.toBeInTheDocument();
};

/**
 * "Drill to detail by" submenu should be present and enabled
 */
const expectDrillToDetailByEnabled = async () => {
  const drillToDetailBy = screen.getByRole('menuitem', {
    name: 'Drill to detail by',
  });

  await expectMenuItemEnabled(drillToDetailBy);
  userEvent.hover(
    within(drillToDetailBy).getByRole('button', { name: 'Drill to detail by' }),
  );

  expect(
    await screen.findByTestId('drill-to-detail-by-submenu'),
  ).toBeInTheDocument();
};

/**
 * "Drill to detail by" submenu should be present and disabled
 */
const expectDrillToDetailByDisabled = async (tooltipContent?: string) => {
  const drillToDetailBySubmenuItem = screen.getByRole('menuitem', {
    name: 'Drill to detail by',
  });

  await expectMenuItemDisabled(drillToDetailBySubmenuItem, tooltipContent);
};

/**
 * "Drill to detail by {dimension}" submenu item should exist and open the correct modal
 */
const expectDrillToDetailByDimension = async (
  filter: BinaryQueryObjectFilterClause,
) => {
  userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' }));
  const drillToDetailBySubMenu = await screen.findByTestId(
    'drill-to-detail-by-submenu',
  );

  const menuItemName = `Drill to detail by ${filter.formattedVal}`;
  const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole(
    'menuitem',
    { name: menuItemName },
  );

  await expectMenuItemEnabled(drillToDetailBySubmenuItem);
  await expectDrillToDetailModal(menuItemName, [filter]);
};

/**
 * "Drill to detail by all" submenu item should exist and open the correct modal
 */
const expectDrillToDetailByAll = async (
  filters: BinaryQueryObjectFilterClause[],
) => {
  userEvent.hover(screen.getByRole('button', { name: 'Drill to detail by' }));
  const drillToDetailBySubMenu = await screen.findByTestId(
    'drill-to-detail-by-submenu',
  );

  const menuItemName = 'Drill to detail by all';
  const drillToDetailBySubmenuItem = within(drillToDetailBySubMenu).getByRole(
    'menuitem',
    { name: menuItemName },
  );

  await expectMenuItemEnabled(drillToDetailBySubmenuItem);
  await expectDrillToDetailModal(menuItemName, filters);
};

beforeAll(() => {
  setupPlugins();
});

test('dropdown menu for unsupported chart', async () => {
  renderMenu({ formData: unsupportedChartFormData });
  await expectDrillToDetailEnabled();
  await expectNoDrillToDetailBy();
});

test('context menu for unsupported chart', async () => {
  renderMenu({
    formData: unsupportedChartFormData,
    isContextMenu: true,
  });

  await expectDrillToDetailEnabled();
  await expectDrillToDetailByDisabled(
    'Drill to detail by value is not yet supported for this chart type.',
  );
});

test('dropdown menu for supported chart, no dimensions', async () => {
  renderMenu({
    formData: noDimensionsFormData,
  });

  await expectDrillToDetailDisabled(
    'Drill to detail is disabled because this chart does not group data by dimension value.',
  );

  await expectNoDrillToDetailBy();
});

test('context menu for supported chart, no dimensions, no filters', async () => {
  renderMenu({
    formData: noDimensionsFormData,
    isContextMenu: true,
  });

  const message =
    'Drill to detail is disabled because this chart does not group data by dimension value.';

  await expectDrillToDetailDisabled(message);
  await expectDrillToDetailByDisabled(message);
});

test('context menu for supported chart, no dimensions, 1 filter', async () => {
  renderMenu({
    formData: noDimensionsFormData,
    isContextMenu: true,
    filters: [filterA],
  });

  const message =
    'Drill to detail is disabled because this chart does not group data by dimension value.';

  await expectDrillToDetailDisabled(message);
  await expectDrillToDetailByDisabled(message);
});

test('dropdown menu for supported chart, dimensions', async () => {
  renderMenu({ formData: defaultFormData });
  await expectDrillToDetailEnabled();
  await expectNoDrillToDetailBy();
});

test('context menu for supported chart, dimensions, no filters', async () => {
  renderMenu({
    formData: defaultFormData,
    isContextMenu: true,
  });

  await expectDrillToDetailEnabled();
  await expectDrillToDetailByDisabled(
    'Right-click on a dimension value to drill to detail by that value.',
  );
});

test('context menu for supported chart, dimensions, 1 filter', async () => {
  const filters = [filterA];
  renderMenu({
    formData: defaultFormData,
    isContextMenu: true,
    filters,
  });

  await expectDrillToDetailEnabled();
  await expectDrillToDetailByEnabled();
  await expectDrillToDetailByDimension(filterA);
});

test('context menu for supported chart, dimensions, 2 filters', async () => {
  const filters = [filterA, filterB];
  renderMenu({
    formData: defaultFormData,
    isContextMenu: true,
    filters,
  });

  await expectDrillToDetailEnabled();
  await expectDrillToDetailByEnabled();
  await expectDrillToDetailByDimension(filterA);
  await expectDrillToDetailByDimension(filterB);
  await expectDrillToDetailByAll(filters);
});