huridocs/uwazi

View on GitHub
app/react/V2/Routes/Settings/Thesauri/specs/Thesauri.spec.tsx

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * @jest-environment jsdom
 */
/* eslint-disable max-statements */
import React, { act } from 'react';
import { IncomingHttpHeaders } from 'http';
import {
  fireEvent,
  RenderResult,
  screen,
  waitFor,
  within,
  render,
  cleanup,
} from '@testing-library/react/pure';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
import { Provider as JotaiProvider } from 'jotai';
import { Provider } from 'react-redux';
import { has } from 'lodash';
import { templatesAtom } from 'app/V2/atoms';
import { atomsGlobalState, reduxStore } from 'V2/shared/testingHelpers';
import { ThesauriList, thesauriLoader } from '../ThesauriList';
import { EditThesaurus } from '../EditThesaurus';
import { editThesaurusLoader } from '../helpers';
import { savedThesaurus, thesauri } from './fixtures';

const deleteFn = jest.fn();
const saveFn = jest.fn();
const mockUseLoaderData = jest
  .fn()
  .mockImplementation((params, _headers) => (!has(params, '_id') ? thesauri : [savedThesaurus]));
const mockDeleteThesauri = deleteFn.mockImplementation((_params, _headers) => ({
  ok: true,
}));
const mockSaveThesauri = saveFn.mockImplementation(_params => savedThesaurus);

jest.mock('app/V2/api/thesauri', () => ({
  __esModule: true,
  default: {
    ...jest.requireActual('app/V2/api/thesauri').default,
    getThesauri: (params: { _id: string }, headers?: IncomingHttpHeaders) =>
      mockUseLoaderData(params, headers),
    delete: (params: { _id: string }) => mockDeleteThesauri(params),
    save: (params: { _id: string }) => mockSaveThesauri(params),
  },
}));

describe('Settings Thesauri', () => {
  beforeEach(() => {
    jest.spyOn(console, 'error').mockImplementation(jest.fn());
    jest.spyOn(console, 'warn').mockImplementation(() => {});
  });

  describe('ThesauriList', () => {
    let renderResult: RenderResult;
    const store = atomsGlobalState();
    store.set(templatesAtom, [
      {
        _id: 'template1',
        name: 'Document',
        properties: [
          {
            _id: 'property1',
            name: 'prop1',
            label: 'property1',
            type: 'select',
            content: 'thesaurus2',
          },
          {
            _id: 'property2',
            name: 'prop2',
            label: 'property2',
            type: 'select',
            content: 'newThesaurus1',
          },
        ],
      },
    ]);
    const router = createMemoryRouter(
      [
        {
          index: true,
          path: '/',
          element: <ThesauriList />,
          loader: thesauriLoader({}),
        },
        { path: '/settings/thesauri/new', element: <EditThesaurus /> },
        {
          path: '/settings/thesauri/edit/newThesaurus1',
          element: <EditThesaurus />,
          loader: editThesaurusLoader({}),
        },
        {
          path: '/edit/newThesaurus1',
          element: <EditThesaurus />,
          loader: editThesaurusLoader({}),
        },
      ],
      {
        initialEntries: ['/'],
      }
    );

    const renderComponent = () =>
      render(
        <Provider store={reduxStore}>
          <JotaiProvider store={store}>
            <RouterProvider router={router} />
          </JotaiProvider>
        </Provider>
      );

    let rows: HTMLElement[];

    const clickOnAction = async (row: number, column: number, role = 'button') => {
      await act(async () => {
        fireEvent.click(within(rows[row].children[column] as HTMLElement).getByRole(role));
      });
    };

    beforeAll(async () => {
      renderResult = renderComponent();
      rows = await waitFor(() => screen.getAllByRole('row'));
    });

    afterAll(() => {
      cleanup();
    });

    const checkRightSaving = async (expectedParams: any) => {
      await act(async () => {
        fireEvent.click(
          within(screen.getByTestId('settings-content-footer')).getByText('Save').parentNode!
        );
      });
      expect(saveFn).toHaveBeenLastCalledWith(expectedParams);
    };

    describe('render existing thesauri', () => {
      it('should show a list of existing thesauri', async () => {
        expect(renderResult.container).toMatchSnapshot();
      });
      it('should not allow to delete used thesaurus', async () => {
        rows = await waitFor(() => screen.getAllByRole('row'));
        const usedThesaurusCheckbox = within(rows[3].children[0] as HTMLElement).getByRole(
          'checkbox'
        );
        expect(usedThesaurusCheckbox).toHaveAttribute('disabled');
      });
    });
    describe('Thesaurus deletion', () => {
      it('should show the selected thesaurus', async () => {
        await clickOnAction(1, 0, 'checkbox');
        await clickOnAction(2, 0, 'checkbox');
        expect(screen.getByTestId('settings-content-footer')).toMatchSnapshot();
        await clickOnAction(0, 0, 'checkbox');
      });

      it('should ask for confirmation to delete the selection', async () => {
        await clickOnAction(1, 0, 'checkbox');
        await clickOnAction(2, 0, 'checkbox');
        await act(async () => {
          fireEvent.click(screen.getByTestId('thesaurus-delete-link'));
        });
        await act(async () => {
          expect(
            screen.getByText('Are you sure you want to delete this item?')
          ).toBeInTheDocument();
          expect(within(screen.getByTestId('modal-body')).getByText('Animals')).toBeInTheDocument();
          expect(within(screen.getByTestId('modal-body')).getByText('Colors')).toBeInTheDocument();
        });
        await act(async () => {
          fireEvent.click(screen.getByTestId('cancel-button'));
        });
        await act(async () => {
          expect(renderResult.container.getElementsByTagName('tbody')).toMatchSnapshot();
        });
        await act(async () => {
          fireEvent.click(screen.getByTestId('thesaurus-delete-link'));
        });
        await act(async () => {
          fireEvent.click(screen.getByTestId('accept-button'));
        });
        await act(async () => {
          expect(deleteFn).toHaveBeenCalledTimes(2);
          expect(deleteFn).toHaveBeenLastCalledWith({ _id: 'thesaurus3' });
        });
        await clickOnAction(0, 0, 'checkbox');
      });
    });
    const editRow = async (label: string, mainValue: string, secondValue: string) => {
      await act(async () => {
        const newItemForm = renderResult.container.getElementsByTagName('form')[1];
        fireEvent.change(within(newItemForm).getByLabelText(label) as HTMLInputElement, {
          target: { value: mainValue },
        });
        fireEvent.change(within(newItemForm).getAllByRole('textbox')[1], {
          target: { value: secondValue },
        });
        fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
      });
    };

    describe('Add Thesaurus', () => {
      it('should add single thesaurus values', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add thesaurus').parentNode!);
        });
        fireEvent.change(screen.getByPlaceholderText('Thesauri name'), {
          target: { value: 'new thesaurus' },
        });
        await act(async () => {
          fireEvent.click(screen.getByText('Add item').parentNode!);
        });
        await editRow('Title', 'single value 1', 'single value 2');
        await act(async () => {
          rows = await waitFor(() => screen.getAllByRole('row'));
          expect(rows.length).toBe(3);
        });
      });

      it('should not add an empty group', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add group').parentNode!);
        });
        let newItemForm: HTMLElement;
        await act(async () => {
          [, newItemForm] = renderResult.container.getElementsByTagName('form');
          fireEvent.change(within(newItemForm).getByLabelText('Name') as HTMLInputElement, {
            target: { value: 'Group 1' },
          });
          fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
        });
        await act(async () => {
          expect(screen.getByText('This field is required')).toBeInTheDocument();
          fireEvent.click(within(newItemForm).getByTestId('thesaurus-form-cancel'));
        });
      });

      it('should add a group to thesaurus', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add group').parentNode!);
        });
        await editRow('Name', 'Group 1', 'Child 1');
        await act(async () => {
          rows = await waitFor(() => screen.getAllByRole('row'));
          expect(rows.length).toBe(4);
        });
      });

      it('should not allow edit the group of an item', async () => {
        await clickOnAction(1, 4, 'button');
        await act(async () => {
          const newItemForm = renderResult.container.getElementsByTagName('form')[1];
          expect(within(newItemForm).getByLabelText('Group')).toHaveAttribute('disabled');
          fireEvent.click(within(newItemForm).getByTestId('thesaurus-form-cancel'));
        });
      });

      it('should not add a row if the item label is empty', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add item').parentNode!);
        });
        await act(async () => {
          fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
        });
        await act(async () => {
          expect(rows.length).toBe(4);
        });
      });

      it('should require the name of the group', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add group').parentNode!);
        });
        let newItemForm: HTMLElement;
        await act(async () => {
          [, newItemForm] = renderResult.container.getElementsByTagName('form');
          fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
        });
        await act(async () => {
          expect(screen.getAllByText('This field is required')).toHaveLength(2);
          fireEvent.click(within(newItemForm).getByTestId('thesaurus-form-cancel'));
        });
      });
      it('should add items into an existing group', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add item').parentNode!);
        });
        await act(async () => {
          const newItemForm = renderResult.container.getElementsByTagName('form')[1];
          fireEvent.change(within(newItemForm).getByLabelText('Title') as HTMLInputElement, {
            target: { value: 'new child 2' },
          });
          fireEvent.click(within(newItemForm).getAllByLabelText('Group')[0]);
        });
        await act(async () => {
          fireEvent.change(screen.getByLabelText('Group'), {
            target: {
              value: (screen.getAllByRole('option')[1] as HTMLInputElement).value,
            },
          });
          fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
        });
        await act(async () => {
          rows = await waitFor(() => screen.getAllByRole('row'));
          expect(rows.length).toBe(4);
        });
      });

      it('should prevent leaving without saving', async () => {
        fireEvent.click(screen.getByText('Cancel').parentNode!);
        await act(async () => {
          expect(screen.getByText('Discard changes')).toBeInTheDocument();
          fireEvent.click(within(screen.getByTestId('modal')).getByText('Cancel').parentNode!);
        });
      });

      it('should save the new thesaurus', async () => {
        await checkRightSaving({
          name: 'new thesaurus',
          values: [
            {
              label: 'single value 1',
            },
            {
              label: 'single value 2',
            },
            {
              label: 'Group 1',
              values: [
                {
                  label: 'Child 1',
                },
                {
                  label: 'new child 2',
                },
              ],
            },
          ],
        });
      });
    });

    describe('Edit Thesaurus', () => {
      it('should edit a root item', async () => {
        rows = await waitFor(() => screen.getAllByRole('row'));
        fireEvent.click(within(rows[1].children[4] as HTMLElement).getByRole('button'));
        await act(async () => {
          const newItemForm = renderResult.container.getElementsByTagName('form')[1];
          fireEvent.change(within(newItemForm).getByLabelText('Title'), {
            target: { value: 'MODIFIED SINGLE VALUE' },
          });
          fireEvent.click(within(newItemForm).getByTestId('thesaurus-form-submit'));
        });
        expect(within(screen.getByRole('table')).getAllByText('MODIFIED SINGLE VALUE').length).toBe(
          2
        );
        await checkRightSaving({
          _id: 'newThesaurus1',
          name: 'new thesaurus',
          values: [
            { id: 'item1', label: 'MODIFIED SINGLE VALUE' },
            { id: 'item2', label: 'single value 2' },
            {
              id: 'item3',
              label: 'Group 1',
              values: [
                {
                  id: 'item3-1',
                  label: 'Child 1',
                },
                { id: 'item3-2', label: 'new child 2' },
              ],
            },
          ],
        });
      });

      it('should edit a root group', async () => {
        rows = await waitFor(() => screen.getAllByRole('row'));
        fireEvent.click(within(rows[3].children[4] as HTMLElement).getByRole('button'));
        await act(async () => {
          const newItemForm = renderResult.container.getElementsByTagName('form')[1];
          fireEvent.change(within(newItemForm).getAllByRole('textbox')[0], {
            target: { value: 'CHANGED GROUP' },
          });
          fireEvent.change(within(newItemForm).getAllByRole('textbox')[3], {
            target: { value: 'ADDED CHILD' },
          });
          fireEvent.click(within(newItemForm).getByTestId('thesaurus-form-submit'));
        });
        await checkRightSaving({
          _id: 'newThesaurus1',
          name: 'new thesaurus',
          values: [
            { id: 'item1', label: 'MODIFIED SINGLE VALUE' },
            { id: 'item2', label: 'single value 2' },
            {
              id: 'item3',
              label: 'CHANGED GROUP',
              values: [
                {
                  id: 'item3-1',
                  label: 'Child 1',
                },
                { id: 'item3-2', label: 'new child 2' },
                { label: 'ADDED CHILD' },
              ],
            },
          ],
        });
      });
      it('should add additional items', async () => {
        await act(async () => {
          fireEvent.click(screen.getByText('Add item').parentNode!);
        });
        let newItemForm: HTMLElement;
        await act(async () => {
          [, newItemForm] = renderResult.container.getElementsByTagName('form');
          fireEvent.change(within(newItemForm).getByLabelText('Title') as HTMLInputElement, {
            target: { value: 'X item' },
          });
          fireEvent.click(within(newItemForm).getAllByLabelText('Group')[0]);
        });

        await act(async () => {
          fireEvent.change(screen.getByLabelText('Group'), {
            target: {
              value: (screen.getAllByRole('option')[1] as HTMLInputElement).value,
            },
          });
          fireEvent.change(within(newItemForm).getAllByRole('textbox')[1], {
            target: { value: 'Additional item' },
          });
          fireEvent.click(screen.getByTestId('thesaurus-form-submit'));
        });
        expect(within(screen.getByRole('table')).getAllByText('Additional item').length).toBe(2);
        await checkRightSaving({
          _id: 'newThesaurus1',
          name: 'new thesaurus',
          values: [
            { id: 'item1', label: 'MODIFIED SINGLE VALUE' },
            { id: 'item2', label: 'single value 2' },
            {
              id: 'item3',
              label: 'CHANGED GROUP',
              values: [
                {
                  id: 'item3-1',
                  label: 'Child 1',
                },
                {
                  id: 'item3-2',
                  label: 'new child 2',
                },
                { label: 'ADDED CHILD' },
                { label: 'X item' },
              ],
            },
            {
              label: 'Additional item',
            },
          ],
        });
      });
      it('should remove an existent item', async () => {
        rows = await waitFor(() => screen.getAllByRole('row'));
        await act(async () => {
          fireEvent.click(within(rows[2].children[1] as HTMLElement).getByRole('checkbox'));
          fireEvent.click(within(rows[3].children[2] as HTMLElement).getByRole('button'));
        });
        rows = await waitFor(() => screen.getAllByRole('row'));
        await act(async () => {
          fireEvent.click(within(rows[5].children[1] as HTMLElement).getByRole('checkbox'));
        });
        await act(async () => {
          fireEvent.click(screen.getByText('Remove').parentNode!);
        });
        await act(async () => {
          expect(
            screen.getByText('Are you sure you want to delete this item?')
          ).toBeInTheDocument();
          fireEvent.click(within(screen.getByTestId('modal')).getByTestId('accept-button'));
        });
        expect(within(screen.getByRole('table')).queryAllByText('single value 2').length).toBe(0);
        await checkRightSaving({
          _id: 'newThesaurus1',
          name: 'new thesaurus',
          values: [
            { id: 'item1', label: 'MODIFIED SINGLE VALUE' },
            {
              id: 'item3',
              label: 'CHANGED GROUP',
              values: [
                {
                  id: 'item3-1',
                  label: 'Child 1',
                },
                { label: 'ADDED CHILD' },
                { label: 'X item' },
              ],
            },
            {
              label: 'Additional item',
            },
          ],
        });
      });
      it('should sort the items', async () => {
        await act(async () => {
          fireEvent.click(
            within(screen.getByTestId('settings-content-footer')).getByText('Sort').parentNode!
          );
        });
        await checkRightSaving({
          _id: 'newThesaurus1',
          name: 'new thesaurus',
          values: [
            { label: 'Additional item' },
            {
              id: 'item3',
              label: 'CHANGED GROUP',
              values: [
                { label: 'ADDED CHILD' },
                {
                  id: 'item3-1',
                  label: 'Child 1',
                },
                { label: 'X item' },
              ],
            },
            {
              id: 'item1',
              label: 'MODIFIED SINGLE VALUE',
            },
          ],
        });
      });
    });
  });
});