diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/trash/__tests__/trashModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/views/trash/__tests__/trashModal.spec.js
index 80225b2027..a3d5e8bf8d 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/views/trash/__tests__/trashModal.spec.js
+++ b/contentcuration/contentcuration/frontend/channelEdit/views/trash/__tests__/trashModal.spec.js
@@ -1,171 +1,259 @@
-import { mount } from '@vue/test-utils';
-import TrashModal from '../TrashModal';
+import { render, screen, waitFor, configure } from '@testing-library/vue';
+import userEvent from '@testing-library/user-event';
+import VueRouter from 'vue-router';
+
import { factory } from '../../../store';
-import router from '../../../router';
import { RouteNames } from '../../../constants';
+import TrashModal from '../TrashModal';
const store = factory();
+const TRASH_ID = 'trash-root-id';
+
const testChildren = [
- {
- id: 'test1',
- title: 'Item',
- kind: 'video',
- modified: new Date(2020, 1, 20),
- },
- {
- id: 'test2',
- title: 'Item',
- kind: 'audio',
- modified: new Date(2020, 2, 1),
- },
- {
- id: 'test3',
- title: 'Topic',
- kind: 'topic',
- modified: new Date(2020, 1, 1),
- },
+ { id: 'test1', title: 'Item', kind: 'video', modified: new Date(2020, 1, 20) },
+ { id: 'test2', title: 'Item', kind: 'audio', modified: new Date(2020, 2, 1) },
+ { id: 'test3', title: 'Topic', kind: 'topic', modified: new Date(2020, 1, 1) },
];
-function makeWrapper(items) {
- const loadContentNodes = jest.spyOn(TrashModal.methods, 'loadContentNodes');
- loadContentNodes.mockImplementation(() => Promise.resolve());
- const loadAncestors = jest.spyOn(TrashModal.methods, 'loadAncestors');
- loadAncestors.mockImplementation(() => Promise.resolve());
- const loadChildren = jest.spyOn(TrashModal.methods, 'loadChildren');
- loadChildren.mockImplementation(() => Promise.resolve({ more: null, results: [] }));
-
- const wrapper = mount(TrashModal, {
- store,
- router,
- computed: {
- currentChannel() {
- return {
- id: 'current channel',
- };
- },
- trashId() {
- return 'trash';
- },
- items() {
- return items || testChildren;
+async function makeWrapper(items = testChildren, isLoading = false) {
+ const loadContentNodesSpy = jest
+ .spyOn(TrashModal.methods, 'loadContentNodes')
+ .mockResolvedValue({});
+ jest.spyOn(TrashModal.methods, 'loadAncestors').mockResolvedValue();
+ jest.spyOn(TrashModal.methods, 'removeContentNodes').mockResolvedValue();
+ const loadNodesSpy = jest.spyOn(TrashModal.methods, 'loadNodes');
+
+ if (isLoading) {
+ jest.spyOn(TrashModal.methods, 'loadChildren').mockReturnValue(new Promise(() => {}));
+ } else {
+ jest.spyOn(TrashModal.methods, 'loadChildren').mockResolvedValue({
+ more: items === testChildren ? null : { parent: TRASH_ID, page: 2 },
+ results: [],
+ });
+ }
+
+ const router = new VueRouter({
+ routes: [
+ { name: RouteNames.TRASH, path: '/:nodeId/trash', component: TrashModal },
+ {
+ name: RouteNames.TREE_VIEW,
+ path: '/:nodeId/:detailNodeId?',
+ component: { template: '
Tree
' },
},
- offline() {
- return false;
+ ],
+ });
+
+ router.replace({ name: RouteNames.TRASH, params: { nodeId: 'test' } }).catch(() => {});
+
+ const routerPush = jest.spyOn(router, 'push').mockResolvedValue();
+
+ const utils = render(
+ TrashModal,
+ {
+ store,
+ router,
+ stubs: {
+ ResourceDrawer: true,
+ OfflineText: true,
},
- backLink() {
- return {
- name: 'TEST_PARENT',
- };
+ computed: {
+ currentChannel: () => ({ id: 'test-channel-id' }),
+ trashId: () => TRASH_ID,
+ items: () => items,
+ offline: () => false,
+ backLink: () => ({ name: RouteNames.TREE_VIEW, params: { nodeId: 'test' } }),
+ getSelectedTopicAndResourceCountText: () => ids => `${ids.length} items selected`,
+ counts: () => ({ topicCount: 0, resourceCount: 0 }),
},
},
- stubs: {
- ResourceDrawer: true,
- OfflineText: true,
+ localVue => {
+ localVue.use(VueRouter);
},
- });
+ );
- return [wrapper, { loadContentNodes, loadAncestors, loadChildren }];
+ if (!isLoading) {
+ await waitFor(() => {
+ expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
+ });
+ }
+
+ const user = userEvent.setup();
+ return { ...utils, routerPush, user, loadNodesSpy, loadContentNodesSpy };
}
-describe('trashModal', () => {
- let wrapper;
+describe('TrashModal', () => {
+ beforeAll(() => configure({ testIdAttribute: 'data-test' }));
+ afterAll(() => configure({ testIdAttribute: 'data-testid' }));
- beforeEach(async () => {
- [wrapper] = makeWrapper();
- router.replace({ name: RouteNames.TRASH, params: { nodeId: 'test' } }).catch(() => {});
- await wrapper.setData({ loading: false });
+ beforeEach(() => {
+ jest.restoreAllMocks();
});
describe('on load', () => {
- it('should show loading indicator if content is loading', async () => {
- await wrapper.setData({ loading: true });
- expect(wrapper.findComponent('[data-test="loading"]').exists()).toBe(true);
+ it('shows a loading indicator while content is loading', async () => {
+ await makeWrapper(testChildren, true);
+ expect(screen.getByTestId('loading')).toBeInTheDocument();
});
- it('should show empty text if there are no items', async () => {
- const [emptyWrapper] = makeWrapper([]);
- await emptyWrapper.setData({ loading: false });
- expect(emptyWrapper.findComponent('[data-test="empty"]').exists()).toBe(true);
+ it('shows empty text when trash has no items', async () => {
+ await makeWrapper([]);
+ expect(screen.getByTestId('empty')).toBeInTheDocument();
});
- it('should show items in list', () => {
- expect(wrapper.findComponent('[data-test="list"]').exists()).toBe(true);
+ it('shows the item list when trash has items', async () => {
+ await makeWrapper();
+ expect(screen.getByTestId('list')).toBeInTheDocument();
});
});
- describe('on topic tree selection', () => {
- it('clicking item should set previewNodeId', async () => {
- await wrapper.findComponent('[data-test="item"]').trigger('click');
- expect(wrapper.vm.previewNodeId).toBe(testChildren[0].id);
- });
+ describe('on item selection', () => {
+ it('checking an item enables the Delete and Restore buttons', async () => {
+ const { user } = await makeWrapper();
- it('checking item in list should add the item ID to the selected array', () => {
- wrapper
- .findComponent('[data-test="checkbox"]')
- .find('input[type="checkbox"]')
- .element.click();
- expect(wrapper.vm.selected).toEqual(['test1']);
+ expect(screen.getByTestId('delete')).toBeDisabled();
+ expect(screen.getByTestId('restore')).toBeDisabled();
+
+ await user.click(screen.getAllByRole('checkbox')[1]);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('delete')).toBeEnabled();
+ expect(screen.getByTestId('restore')).toBeEnabled();
+ });
});
- it('checking select all checkbox should check all items', () => {
- wrapper.findComponent('[data-test="selectall"]').vm.$emit('input', true);
- expect(wrapper.vm.selected).toEqual(testChildren.map(c => c.id));
+ it('checking the select-all checkbox checks all items', async () => {
+ const { user } = await makeWrapper();
+ await user.click(screen.getByTestId('selectall'));
+
+ await waitFor(() => {
+ screen
+ .getAllByRole('checkbox')
+ .slice(1)
+ .forEach(cb => {
+ expect(cb).toBeChecked();
+ });
+ });
});
});
describe('on close', () => {
- it('clicking close button should go back to parent route', async () => {
- await wrapper.findComponent('[data-test="close"]').trigger('click');
- expect(wrapper.vm.$route.name).toBe('TEST_PARENT');
+ it('clicking the close button navigates back to the tree view', async () => {
+ const { routerPush, user } = await makeWrapper();
+
+ const closeButton = await screen.findByTestId('close');
+ await user.click(closeButton);
+
+ await waitFor(() => {
+ expect(routerPush).toHaveBeenCalledWith(
+ expect.objectContaining({ name: RouteNames.TREE_VIEW }),
+ );
+ });
});
});
describe('on delete', () => {
- it('DELETE button should be disabled if no items are selected', () => {
- expect(wrapper.findComponent('[data-test="delete"]').vm.disabled).toBe(true);
+ it('Delete button is disabled when no items are selected', async () => {
+ await makeWrapper();
+ expect(screen.getByTestId('delete')).toBeDisabled();
});
- it('clicking DELETE button should open delete confirmation dialog', async () => {
- await wrapper.setData({ selected: testChildren.map(c => c.id) });
- await wrapper.findComponent('[data-test="delete"]').trigger('click');
- expect(wrapper.vm.showConfirmationDialog).toBe(true);
+ it('clicking Delete opens a confirmation dialog', async () => {
+ const { user } = await makeWrapper();
+ await user.click(screen.getByTestId('selectall'));
+ await user.click(screen.getByTestId('delete'));
+
+ expect(await screen.findByText(/You cannot undo this action/i)).toBeInTheDocument();
});
- it('clicking CLOSE on delete confirmation dialog should close the dialog', async () => {
- await wrapper.setData({ showConfirmationDialog: true });
- await wrapper.findComponent('[data-test="deleteconfirm"]').vm.$emit('cancel');
- expect(wrapper.vm.showConfirmationDialog).toBe(false);
+ it('clicking Cancel in the confirmation dialog closes it', async () => {
+ const { user } = await makeWrapper();
+ await user.click(screen.getByTestId('selectall'));
+ await user.click(screen.getByTestId('delete'));
+ await screen.findByText(/You cannot undo this action/i);
+
+ await user.click(screen.getByRole('button', { name: /Cancel/i }));
+
+ await waitFor(() => {
+ expect(screen.queryByText(/You cannot undo this action/i)).not.toBeInTheDocument();
+ });
});
- it('clicking DELETE PERMANENTLY on delete confirmation dialog should trigger deletion', async () => {
- const selected = testChildren.map(c => c.id);
- const deleteContentNodes = jest.spyOn(wrapper.vm, 'deleteContentNodes');
- deleteContentNodes.mockImplementation(() => Promise.resolve());
- await wrapper.setData({ selected, showConfirmationDialog: true });
- await wrapper.findComponent('[data-test="deleteconfirm"]').vm.$emit('submit');
- expect(deleteContentNodes).toHaveBeenCalledWith(selected);
+ it('clicking Delete permanently calls deleteContentNodes with the selected IDs', async () => {
+ const deleteContentNodes = jest
+ .spyOn(TrashModal.methods, 'deleteContentNodes')
+ .mockResolvedValue();
+
+ const { user } = await makeWrapper();
+
+ await user.click(screen.getByTestId('selectall'));
+ await user.click(screen.getByTestId('delete'));
+ await user.click(await screen.findByRole('button', { name: /Delete permanently/i }));
+
+ await waitFor(() => {
+ expect(deleteContentNodes).toHaveBeenCalledWith(testChildren.map(c => c.id));
+ });
+ });
+
+ it('successful deletion triggers snackbar and reloads nodes', async () => {
+ jest.spyOn(TrashModal.methods, 'deleteContentNodes').mockResolvedValue();
+ const dispatchSpy = jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
+
+ const { user, loadNodesSpy } = await makeWrapper();
+ await user.click(screen.getByTestId('selectall'));
+ await user.click(screen.getByTestId('delete'));
+ await user.click(await screen.findByRole('button', { name: /Delete permanently/i }));
+
+ await waitFor(() => {
+ expect(dispatchSpy).toHaveBeenCalledWith('showSnackbar', {
+ text: 'Permanently deleted',
+ });
+ expect(loadNodesSpy).toHaveBeenCalled();
+ });
});
});
describe('on restore', () => {
- it('RESTORE button should be disabled if no items are selected', () => {
- expect(wrapper.findComponent('[data-test="restore"]').vm.disabled).toBe(true);
+ it('Restore button is disabled when no items are selected', async () => {
+ await makeWrapper();
+ expect(screen.getByTestId('restore')).toBeDisabled();
});
- it('RESTORE should set moveModalOpen to true', async () => {
- const selected = testChildren.map(c => c.id);
- await wrapper.setData({ selected });
- await wrapper.findComponent('[data-test="restore"]').trigger('click');
- expect(wrapper.vm.moveModalOpen).toBe(true);
+ it('clicking Restore opens the MoveModal', async () => {
+ const { user } = await makeWrapper();
+ await user.click(screen.getByTestId('selectall'));
+ await user.click(screen.getByTestId('restore'));
+ expect(await screen.findByRole('button', { name: /Move here/i })).toBeInTheDocument();
});
+ });
+
+ describe('selection count', () => {
+ it('shows selected item count in the bottom bar', async () => {
+ const { user } = await makeWrapper();
+
+ expect(screen.queryByText(/items selected/i)).not.toBeInTheDocument();
+
+ await user.click(screen.getByTestId('selectall'));
+
+ expect(await screen.findByText(`${testChildren.length} items selected`)).toBeInTheDocument();
+ });
+ });
+
+ describe('pagination', () => {
+ it('shows a Show more button when there is more paginated content', async () => {
+ await makeWrapper([testChildren[0]]);
+ expect(await screen.findByRole('button', { name: /Show more/i })).toBeInTheDocument();
+ });
+
+ it('clicking Show more calls loadContentNodes with pagination params', async () => {
+ const { user, loadContentNodesSpy } = await makeWrapper([testChildren[0]]);
+ const showMoreBtn = await screen.findByRole('button', { name: /Show more/i });
+
+ await user.click(showMoreBtn);
- it('moveNoves should clear selected and previewNodeId', async () => {
- const moveContentNodes = jest.spyOn(wrapper.vm, 'moveContentNodes');
- moveContentNodes.mockImplementation(() => Promise.resolve());
- wrapper.vm.moveNodes();
- expect(wrapper.vm.selected).toEqual([]);
- expect(wrapper.vm.previewNodeId).toBe(null);
+ await waitFor(() => {
+ expect(loadContentNodesSpy).toHaveBeenCalledWith({ parent: TRASH_ID, page: 2 });
+ });
});
});
});