grommet/grommet

View on GitHub
src/js/components/Data/__tests__/Data-test.tsx

Summary

Maintainability
D
2 days
Test Coverage
import React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen } from '@testing-library/react';
import 'jest-styled-components';

import { Grommet } from '../../Grommet';
import { DataFilter } from '../../DataFilter';
import { DataFilters } from '../../DataFilters';
import { DataTable } from '../../DataTable';
import { Pagination } from '../../Pagination';
import { Toolbar } from '../../Toolbar';
import { Data } from '..';
import { createPortal, expectPortal } from '../../../utils/portal';
const DEBOUNCE_TIMEOUT = 300;

// asserts that AnnounceContext aria-live region and visible DataSummary each have this text
const expectDataSummary = (message: string) =>
  expect(screen.getAllByText(message)).toHaveLength(2);

const data = [
  {
    name: 'aa',
    enabled: true,
    rating: 2.3,
    sub: { note: 'ZZ' },
    tags: ['qa', 'staging', 'prod'],
  },
  {
    name: 'bb',
    enabled: false,
    rating: 4.3,
    sub: { note: 'YY' },
    tags: ['qa', 'staging'],
  },
  { name: 'cc', sub: {}, tags: ['qa'] },
  { name: 'dd' },
];

describe('Data', () => {
  window.scrollTo = jest.fn();
  beforeEach(createPortal);

  test('renders', () => {
    const { container } = render(
      <Grommet>
        <Data data={data} />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('toolbar', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('4 items');
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view', () => {
    const { getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          view={{ search: '', properties: {} }}
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expect(getByText('bb')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view search', () => {
    const { getByText, queryByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          view={{ search: 'a' }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('1 result of 4 items');
    expect(getByText('aa')).toBeTruthy();
    expect(queryByText('bb')).toBeFalsy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view property option', () => {
    const { getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          view={{
            properties: { enabled: [true] },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('1 result of 4 items');
    expect(getByText('aa')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view property range', () => {
    const { getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
            rating: { label: 'Rating', range: { min: 0, max: 5 } },
          }}
          view={{
            properties: { rating: { min: 3.0, max: 5.0 } },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('1 result of 4 items');
    expect(getByText('bb')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view sort', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          view={{
            sort: { property: 'sub.note', direction: 'desc' },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('4 items');
    expect(container.firstChild).toMatchSnapshot();
  });

  test('view all', () => {
    const { getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          view={{
            search: 'a',
            properties: { enabled: [true] },
            sort: { property: 'name', direction: 'desc' },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('1 result of 4 items');
    expect(getByText('aa')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('messages', () => {
    const { getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          messages={{
            dataSummary: {
              total: '{total} things',
            },
          }}
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
          }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('4 things');
    expect(getByText('aa')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('uncontrolled search', () => {
    jest.useFakeTimers();
    const { getByRole, getByText, queryByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{ name: { label: 'Name' } }}
          view={{ search: '', properties: {} }}
          toolbar
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expectDataSummary('4 items');
    expect(getByText('bb')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.change(getByRole('searchbox'), {
      target: { value: 'a' },
    });
    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expectDataSummary('1 result of 4 items');
    expect(queryByText('bb')).toBeFalsy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('controlled search', () => {
    jest.useFakeTimers();
    const onView = jest.fn();
    const { getByRole, getByText, container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{ name: { label: 'Name' } }}
          view={{ search: '', properties: {} }}
          toolbar
          onView={onView}
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expect(getByText('bb')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.change(getByRole('searchbox'), {
      target: { value: 'a' },
    });
    expect(getByText('bb')).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();

    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expect(onView).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        search: 'a',
        properties: {},
      }),
    );
  });

  test('toolbar search', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{ name: { label: 'Name' } }}
          view={{
            properties: {},
            search: '',
            sort: { property: 'name', direction: 'asc' },
          }}
          toolbar="search"
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('toolbar filters', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={data}
          properties={{ name: { label: 'Name' } }}
          view={{
            properties: {},
            search: '',
            sort: { property: 'name', direction: 'asc' },
          }}
          toolbar="filters"
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('pagination', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={[...data].slice(2, 4)}
          total={data.length}
          properties={{ name: { label: 'Name' } }}
          onView={() => {}}
          view={{ page: 2, step: 2 }}
        >
          <DataTable />
          <Pagination />
        </Data>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('pagination step', () => {
    const { container } = render(
      <Grommet>
        <Data
          data={[...data].slice(2, 4)}
          total={data.length}
          properties={{ name: { label: 'Name' } }}
          onView={() => {}}
        >
          <DataTable />
          <Pagination step={2} page={2} />
        </Data>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('onView', () => {
    const onView = jest.fn();
    const { container } = render(
      <Grommet>
        <Data data={data} onView={onView}>
          <DataFilters />
          <DataTable />
        </Data>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('properties when property is an array', () => {
    const { asFragment } = render(
      <Grommet>
        <Data
          data={data}
          toolbar
          properties={{
            name: { label: 'Name' },
            'sub.note': { label: 'Note' },
            tags: {
              label: 'Tags',
              options: [
                { label: '01 - Development', value: 'dev' },
                { label: '02 - QA', value: 'qa' },
                { label: '03 - Staging', value: 'staging' },
                { label: '04 - Production', value: 'prod' },
              ],
            },
          }}
        >
          <DataTable
            columns={[
              { property: 'name', header: 'Name' },
              { property: 'sub.note', header: 'Note' },
              {
                property: 'tags',
                header: 'Tags',
                render: ({ tags }) => (tags ? tags.join(', ') : null),
              },
            ]}
          />
        </Data>
      </Grommet>,
    );

    expect(asFragment()).toMatchSnapshot();
    const filtersButton = screen.getByRole('button', { name: 'Open filters' });
    expect(filtersButton).toBeTruthy();
    fireEvent.click(filtersButton);
    expect(screen.getByRole('button', { name: 'Apply filters' })).toBeTruthy();
    expectPortal('data--filters-control').toMatchSnapshot();
  });

  test('should include badge based on view', () => {
    render(
      <Grommet>
        <Data
          data={data}
          view={{
            properties: { rating: { min: 3, max: 5 }, enabled: [true] },
          }}
          toolbar="filters"
        >
          <DataTable />
        </Data>
      </Grommet>,
    );

    const filterButton = screen.getByRole('button', {
      name: 'Open filters, 2 filters applied',
    });

    expect(filterButton).toBeTruthy();
  });

  test('should not include properties with badge: false in badge count', () => {
    render(
      <Grommet>
        <Data
          data={data}
          properties={{
            name: {
              badge: false,
            },
            enabled: {},
            rating: {},
          }}
        >
          <Toolbar>
            <DataFilter property="name" />
            <DataFilters layer>
              <DataFilter property="enabled" />
              <DataFilter property="rating" />
            </DataFilters>
          </Toolbar>
        </Data>
      </Grommet>,
    );

    const checkBox = screen.getByText('aa');
    fireEvent.click(checkBox);

    // no badge should be applied, so expect default tip
    const filterButton = screen.getByRole('button', {
      name: 'Open filters',
    });

    expect(filterButton).toBeTruthy();
  });

  test('uncontrolled, when paginated, search should return to page 1', () => {
    const properties = {
      name: { label: 'Name' },
    };
    const view = {
      step: 2,
      page: 1,
      search: '',
    };
    const columns = [
      { property: 'name', header: 'Name', size: 'small', primary: true },
    ];

    jest.useFakeTimers();
    const { getByRole, getByText, container } = render(
      <Grommet>
        <Data data={data} properties={properties} view={view} toolbar>
          <DataTable columns={columns} />
          <Pagination />
        </Data>
      </Grommet>,
    );

    const page1Button = getByRole('button', { name: 'Go to page 1' });
    const page2Button = getByRole('button', { name: 'Go to page 2' });
    const searchBox = getByRole('searchbox');

    fireEvent.click(page2Button);
    expect(getByText('cc')).toBeTruthy();
    expect(page1Button).not.toHaveAttribute('aria-current', 'page');
    expect(page2Button).toHaveAttribute('aria-current', 'page');
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.change(searchBox, {
      target: { value: 'a' },
    });
    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expect(getByText('aa')).toBeTruthy();
    expect(page1Button).toHaveAttribute('aria-current', 'page');
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.change(searchBox, {
      target: { value: '' },
    });
    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expect(getByText('bb')).toBeTruthy();
    expect(page1Button).toHaveAttribute('aria-current', 'page');
    expect(page2Button).not.toHaveAttribute('aria-current', 'page');
    expect(container.firstChild).toMatchSnapshot();
  });

  test('controlled, when paginated, search should call onView with page 1', () => {
    const properties = {
      name: { label: 'Name' },
    };
    const view = {
      step: 2,
      page: 1,
      search: '',
    };
    const columns = [
      { property: 'name', header: 'Name', size: 'small', primary: true },
    ];

    jest.useFakeTimers();
    const onView = jest.fn();
    const { getByRole } = render(
      <Grommet>
        <Data
          data={data}
          properties={properties}
          view={view}
          toolbar
          onView={onView}
        >
          <DataTable columns={columns} />
          <Pagination />
        </Data>
      </Grommet>,
    );

    const page1Button = getByRole('button', { name: 'Go to page 1' });
    const page2Button = getByRole('button', { name: 'Go to page 2' });
    const searchBox = getByRole('searchbox');

    fireEvent.click(page2Button);
    expect(page1Button).not.toHaveAttribute('aria-current', 'page');
    expect(page2Button).toHaveAttribute('aria-current', 'page');
    expect(onView).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        page: 2,
      }),
    );

    fireEvent.change(searchBox, {
      target: { value: 'a' },
    });
    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expect(onView).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        search: 'a',
        page: 1,
      }),
    );

    fireEvent.change(searchBox, {
      target: { value: '' },
    });
    act(() => {
      jest.advanceTimersByTime(DEBOUNCE_TIMEOUT);
    });
    expect(onView).toHaveBeenNthCalledWith(
      3,
      expect.objectContaining({
        search: '',
        page: 1,
      }),
    );
  });
});