Skip to content
Original file line number Diff line number Diff line change
@@ -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]);
});
});
Loading