diff --git a/kolibri/plugins/device/frontend/views/__tests__/RearrangeChannelsPage.spec.js b/kolibri/plugins/device/frontend/views/__tests__/RearrangeChannelsPage.spec.js index 006c16a7637..0132ecbbcd1 100644 --- a/kolibri/plugins/device/frontend/views/__tests__/RearrangeChannelsPage.spec.js +++ b/kolibri/plugins/device/frontend/views/__tests__/RearrangeChannelsPage.spec.js @@ -1,98 +1,163 @@ -import { shallowMount } from '@vue/test-utils'; -import useUser, { useUserMock } from 'kolibri/composables/useUser'; // eslint-disable-line -import useSnackbar, { useSnackbarMock } from 'kolibri/composables/useSnackbar'; // eslint-disable-line -import makeStore from '../../__tests__/utils/makeStore'; +import { render, screen, waitFor, fireEvent } from '@testing-library/vue'; +import VueRouter from 'vue-router'; +import useUser, { useUserMock } from 'kolibri/composables/useUser'; // eslint-disable-line import-x/named +import useSnackbar, { useSnackbarMock } from 'kolibri/composables/useSnackbar'; // eslint-disable-line import-x/named +import { createTranslator } from 'kolibri/utils/i18n'; +import { dragSortStrings } from 'kolibri-common/components/sortable/dragSortStrings'; import RearrangeChannelsPage from '../RearrangeChannelsPage'; +import makeStore from '../../__tests__/utils/makeStore'; +import { PageNames } from '../../constants'; + +const { moveItemUpLabel$, moveItemDownLabel$ } = dragSortStrings; + +const { instructions$, noChannels$, successNotification$, failureNotification$ } = createTranslator( + RearrangeChannelsPage.name, + RearrangeChannelsPage.$trs, +); jest.mock('../../composables/useContentTasks'); +jest.mock('kolibri-common/composables/usePageLoading'); +jest.mock( + 'sortablejs', + () => + jest.fn().mockImplementation((el, options) => ({ + destroy: jest.fn(), + options, + })), + { virtual: true }, +); jest.mock('kolibri/composables/useUser'); jest.mock('kolibri/composables/useSnackbar'); -RearrangeChannelsPage.methods.postNewOrder = () => Promise.resolve(); -RearrangeChannelsPage.methods.fetchChannels = () => { - return Promise.resolve([ - { id: '1', name: 'Channel 1' }, - { id: '2', name: 'Channel 2' }, - ]); -}; -async function makeWrapper() { - const store = makeStore(); - useUser.mockImplementation(() => useUserMock({ canManageContent: true })); - const wrapper = shallowMount(RearrangeChannelsPage, { - store, +function createRouter() { + return new VueRouter({ + routes: [ + { name: PageNames.REARRANGE_CHANNELS, path: '/content/reorder_channels' }, + { name: PageNames.MANAGE_CONTENT_PAGE, path: '/content' }, + ], }); - // Have to wait to let the channels data load - await global.flushPromises(); - return { wrapper }; } +const MOCK_CHANNELS = [ + { id: '1', name: 'Channel 1' }, + { id: '2', name: 'Channel 2' }, +]; + describe('RearrangeChannelsPage', () => { - const createSnackbar = jest.fn(); - beforeAll(() => { + let createSnackbar; + + const renderComponent = async () => { + createSnackbar = jest.fn(); + useUser.mockImplementation(() => useUserMock({ canManageContent: true })); useSnackbar.mockImplementation(() => useSnackbarMock({ createSnackbar })); + + const store = makeStore(); + const router = createRouter(); + await router.push({ name: PageNames.REARRANGE_CHANNELS }); + return render(RearrangeChannelsPage, { store, router }); + }; + + beforeEach(() => { + jest.clearAllMocks(); + RearrangeChannelsPage.methods.fetchChannels = () => Promise.resolve(MOCK_CHANNELS); + RearrangeChannelsPage.methods.postNewOrder = () => Promise.resolve(); }); - async function simulateSort(wrapper) { - const dragContainer = wrapper.findComponent({ name: 'DragContainer' }); - dragContainer.vm.$emit('sort', { - newArray: [wrapper.vm.channels[1], wrapper.vm.channels[0]], + it('loads the data on mount', async () => { + await renderComponent(); + await waitFor(() => { + expect(screen.getByText(MOCK_CHANNELS[0].name)).toBeInTheDocument(); + expect(screen.getByText(MOCK_CHANNELS[1].name)).toBeInTheDocument(); }); - expect(wrapper.vm.postNewOrder).toHaveBeenCalledWith(['2', '1']); - await global.flushPromises(); - } + }); - it('loads the data on mount', async () => { - const { wrapper } = await makeWrapper(); - expect(wrapper.vm.loading).toBe(false); - expect(wrapper.vm.channels).toHaveLength(2); + it('shows the instructions text', async () => { + await renderComponent(); + await waitFor(() => { + expect(screen.getByText(instructions$())).toBeInTheDocument(); + }); + }); + + it('shows a message when there are no channels', async () => { + RearrangeChannelsPage.methods.fetchChannels = () => Promise.resolve([]); + await renderComponent(); + await waitFor(() => { + expect(screen.getByText(noChannels$())).toBeInTheDocument(); + }); }); it('handles a successful @sort event properly', async () => { - const { wrapper } = await makeWrapper(); - wrapper.vm.postNewOrder = jest.fn().mockResolvedValue(); - wrapper.vm.$store.dispatch = jest.fn(); - await simulateSort(wrapper); - expect(createSnackbar).toHaveBeenCalledWith('Channel order saved'); - expect(wrapper.vm.channels[0].id).toEqual('2'); - expect(wrapper.vm.channels[1].id).toEqual('1'); + const Sortable = require('sortablejs'); + await renderComponent(); + await waitFor(() => screen.getByText(MOCK_CHANNELS[0].name)); + + // Simulate the drag ending by calling the onEnd callback SortableJS + // would normally call itself once the pointer is released. + const { onEnd } = Sortable.mock.results[0].value.options; + onEnd({ oldIndex: 0, newIndex: 1, item: document.createElement('div') }); + + await waitFor(() => { + expect(createSnackbar).toHaveBeenCalledWith(successNotification$()); + }); + const channelNames = MOCK_CHANNELS.map(channel => channel.name); + const matchesChannelName = text => channelNames.includes(text.trim()); + const titles = screen.getAllByText(matchesChannelName).map(el => el.textContent.trim()); + expect(titles).toEqual([MOCK_CHANNELS[1].name, MOCK_CHANNELS[0].name]); }); - it('handles a failed @sort event properly', async () => { - const { wrapper } = await makeWrapper(); - wrapper.vm.postNewOrder = jest.fn().mockRejectedValue(); - wrapper.vm.$store.dispatch = jest.fn(); - await simulateSort(wrapper); - expect(createSnackbar).toHaveBeenCalledWith('There was a problem reordering the channels'); - // Channels array is reset after an error - expect(wrapper.vm.channels[0].id).toEqual('1'); - expect(wrapper.vm.channels[1].id).toEqual('2'); + it('handles a moveUp event properly', async () => { + await renderComponent(); + await waitFor(() => screen.getByText(MOCK_CHANNELS[0].name)); + + const upButtons = screen.getAllByRole('button', { + name: moveItemUpLabel$({ item: MOCK_CHANNELS[1].name }), + }); + await fireEvent.click(upButtons[0]); + + await waitFor(() => { + expect(createSnackbar).toHaveBeenCalledWith(successNotification$()); + }); + const channelNames = MOCK_CHANNELS.map(channel => channel.name); + const matchesChannelName = text => channelNames.includes(text.trim()); + const titles = screen.getAllByText(matchesChannelName).map(el => el.textContent.trim()); + expect(titles).toEqual([MOCK_CHANNELS[1].name, MOCK_CHANNELS[0].name]); }); - // Will mock the handleOrderChange method to test these cases synchronousy, - // since that method should be tested by the previous tests. - it('handles a @moveUp event properly', async () => { - const { wrapper } = await makeWrapper(); - const spy = (wrapper.vm.handleOrderChange = jest.fn()); - const dragSortWidget = wrapper.findAllComponents({ name: 'DragSortWidget' }).at(1); - dragSortWidget.vm.$emit('moveUp'); - expect(spy).toHaveBeenCalledWith({ - newArray: [ - { id: '2', name: 'Channel 2' }, - { id: '1', name: 'Channel 1' }, - ], + it('handles a failed @sort event properly', async () => { + const Sortable = require('sortablejs'); + RearrangeChannelsPage.methods.postNewOrder = () => Promise.reject(); + await renderComponent(); + await waitFor(() => screen.getByText(MOCK_CHANNELS[0].name)); + + const { onEnd } = Sortable.mock.results[0].value.options; + onEnd({ oldIndex: 0, newIndex: 1, item: document.createElement('div') }); + + await waitFor(() => { + expect(createSnackbar).toHaveBeenCalledWith(failureNotification$()); + }); + await waitFor(() => { + const channelNames = MOCK_CHANNELS.map(channel => channel.name); + const matchesChannelName = text => channelNames.includes(text.trim()); + const titles = screen.getAllByText(matchesChannelName).map(el => el.textContent.trim()); + expect(titles).toEqual([MOCK_CHANNELS[0].name, MOCK_CHANNELS[1].name]); }); }); - it('handles a @moveDown event properly', async () => { - const { wrapper } = await makeWrapper(); - const spy = (wrapper.vm.handleOrderChange = jest.fn()); - const dragSortWidget = wrapper.findAllComponents({ name: 'DragSortWidget' }).at(0); - dragSortWidget.vm.$emit('moveDown'); - expect(spy).toHaveBeenCalledWith({ - newArray: [ - { id: '2', name: 'Channel 2' }, - { id: '1', name: 'Channel 1' }, - ], + it('handles a moveDown event properly', async () => { + await renderComponent(); + await waitFor(() => screen.getByText(MOCK_CHANNELS[0].name)); + + const downButtons = screen.getAllByRole('button', { + name: moveItemDownLabel$({ item: MOCK_CHANNELS[0].name }), + }); + await fireEvent.click(downButtons[0]); + + await waitFor(() => { + expect(createSnackbar).toHaveBeenCalledWith(successNotification$()); }); + const channelNames = MOCK_CHANNELS.map(channel => channel.name); + const matchesChannelName = text => channelNames.includes(text.trim()); + const titles = screen.getAllByText(matchesChannelName).map(el => el.textContent.trim()); + expect(titles).toEqual([MOCK_CHANNELS[1].name, MOCK_CHANNELS[0].name]); }); });