diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/CatalogList.vue b/contentcuration/contentcuration/frontend/channelList/views/Channel/CatalogList.vue index 5b245e8239..6cd053fcbe 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/CatalogList.vue +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/CatalogList.vue @@ -33,7 +33,6 @@ wrap class="list-wrapper" > - @@ -53,7 +52,6 @@ v-model="selectAll" class="mb-4 mx-2" :label="$tr('selectAll')" - data-test="select-all" :indeterminate="selected.length > 0 && selected.length < channels.length" /> @@ -68,7 +66,6 @@ v-model="selected" class="mx-2" :value="item.id" - data-test="checkbox" />
@@ -101,7 +97,6 @@
@@ -109,7 +104,6 @@ Promise.resolve()); - - const downloadCSV = jest.spyOn(CatalogList.methods, 'downloadCSV'); - const downloadPDF = jest.spyOn(CatalogList.methods, 'downloadPDF'); - - const wrapper = mount(CatalogList, { - router, - store, - computed: { - page() { - return { - count: results.length, - results, - }; +const localVue = createLocalVue(); +localVue.use(Vuex); +localVue.use(VueRouter); + +const mockChannels = [ + { + id: 'channel-1', + name: 'Channel 1', + description: 'Test channel 1', + language: 'en', + modified: new Date('2024-01-15'), + last_published: new Date('2024-01-10'), + }, + { + id: 'channel-2', + name: 'Channel 2', + description: 'Test channel 2', + language: 'en', + modified: new Date('2024-01-20'), + last_published: new Date('2024-01-18'), + }, +]; + +const mockChannelIds = mockChannels.map(c => c.id); + +function createMockStore() { + const mockSearchCatalog = jest.fn(() => Promise.resolve()); + + return { + store: new Store({ + state: { + connection: { online: true }, }, - ...computed, - }, - stubs: { - CatalogFilters: true, - }, - }); - return [wrapper, { loadCatalog, downloadCSV, downloadPDF }]; + getters: { + loggedIn: () => true, + }, + actions: { + showSnackbar: jest.fn(), + }, + modules: { + channel: { + namespaced: true, + state: { + channelsMap: Object.fromEntries(mockChannels.map(c => [c.id, c])), + }, + getters: { + getChannels: state => ids => ids.map(id => state.channelsMap[id]).filter(Boolean), + getChannel: state => id => state.channelsMap[id], + }, + }, + channelList: { + namespaced: true, + state: { + page: { + count: mockChannelIds.length, + results: mockChannelIds, + }, + }, + actions: { + searchCatalog: mockSearchCatalog, + }, + }, + }, + }), + mockSearchCatalog, + }; } -describe('catalogFilterBar', () => { - let wrapper, mocks; - - beforeEach(async () => { - [wrapper, mocks] = makeWrapper(); - await wrapper.setData({ loading: false }); +function createMockRouter() { + const router = new VueRouter({ + routes: [ + { name: RouteNames.CATALOG_ITEMS, path: '/catalog' }, + { name: RouteNames.CATALOG_DETAILS, path: '/catalog/:channelId' }, + ], }); + router.push({ name: RouteNames.CATALOG_ITEMS }).catch(() => {}); + return router; +} - it('should call loadCatalog on mount', () => { - [wrapper, mocks] = makeWrapper(); - expect(mocks.loadCatalog).toHaveBeenCalled(); - }); +function renderComponent() { + const { store, mockSearchCatalog } = createMockStore(); + const router = createMockRouter(); + + return { + ...render(CatalogList, { + localVue, + store, + router, + stubs: { CatalogFilters: true }, + }), + router, + mockSearchCatalog, + }; +} - describe('on query change', () => { - const searchCatalogMock = jest.fn(); +describe('CatalogList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); - beforeEach(() => { - router.replace({ query: {} }).catch(() => {}); - searchCatalogMock.mockReset(); - [wrapper, mocks] = makeWrapper({ - debouncedSearch() { - return searchCatalogMock; - }, - }); + it('calls searchCatalog on mount', async () => { + const { mockSearchCatalog } = renderComponent(); + await waitFor(() => { + expect(mockSearchCatalog).toHaveBeenCalled(); }); + }); - it('should call debouncedSearch', async () => { - const keywords = 'search catalog test'; - router.push({ query: { keywords } }).catch(() => {}); - await wrapper.vm.$nextTick(); - expect(searchCatalogMock).toHaveBeenCalled(); + it('renders title', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText(/results found/i)).toBeInTheDocument(); }); + }); - it('should reset excluded if a filter changed', async () => { - const keywords = 'search reset test'; - await wrapper.setData({ excluded: ['item 1'] }); - router.push({ query: { keywords } }).catch(() => {}); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.excluded).toEqual([]); + it('displays download button', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('select')).toBeInTheDocument(); }); + }); - it('should keep excluded if page number changed', async () => { - await wrapper.setData({ excluded: ['item 1'] }); - router - .push({ - query: { - ...wrapper.vm.$route.query, - page: 2, - }, - }) - .catch(() => {}); - await wrapper.vm.$nextTick(); - expect(wrapper.vm.excluded).toEqual(['item 1']); + it('renders channel cards', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Channel 1')).toBeInTheDocument(); + expect(screen.getByText('Channel 2')).toBeInTheDocument(); }); }); - describe('download workflow', () => { - describe('toggling selection mode', () => { - it('checkboxes and toolbar should be hidden if selecting is false', () => { - expect(wrapper.findComponent('[data-test="checkbox"]').exists()).toBe(false); - expect(wrapper.findComponent('[data-test="toolbar"]').exists()).toBe(false); - }); - - it('should activate when select button is clicked', async () => { - await wrapper.findComponent('[data-test="select"]').trigger('click'); - expect(wrapper.vm.selecting).toBe(true); - }); + describe('selection', () => { + it('hides checkboxes and selection text initially', async () => { + renderComponent(); + await waitFor(() => screen.getByTestId('select')); - it('clicking cancel should exit selection mode', async () => { - await wrapper.setData({ selecting: true }); - await wrapper.findComponent('[data-test="cancel"]').trigger('click'); - expect(wrapper.vm.selecting).toBe(false); - }); - - it('excluded should reset when selection mode is exited', async () => { - await wrapper.setData({ selecting: true, excluded: ['item-1', 'item-2'] }); - wrapper.vm.setSelection(false); - expect(wrapper.vm.excluded).toHaveLength(0); - }); + expect(screen.queryByRole('checkbox', { name: /select all/i })).not.toBeInTheDocument(); + expect(screen.queryByText(/channels selected/i)).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument(); }); - describe('selecting channels', () => { - const excluded = ['item-1']; + it('shows checkboxes, selection text, and cancel button when selecting', async () => { + const user = userEvent.setup(); + renderComponent(); - beforeEach(async () => { - await wrapper.setData({ - selecting: true, - excluded, - }); - }); - - it('selecting all should select all items on the page', async () => { - await wrapper.setData({ excluded: excluded.concat(results) }); - wrapper.vm.selectAll = true; - expect(wrapper.vm.excluded).toEqual(excluded); - expect(wrapper.vm.selected).toEqual(results); - }); + const selectButton = await waitFor(() => screen.getByTestId('select')); + await user.click(selectButton); - it('deselecting all should select all items on the page', () => { - wrapper.vm.selectAll = false; - expect(wrapper.vm.excluded).toEqual(excluded.concat(results)); - expect(wrapper.vm.selected).toEqual([]); + await waitFor(() => { + expect(screen.queryByRole('checkbox', { name: /select all/i })).toBeInTheDocument(); + expect(screen.queryByText(/channels selected/i)).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /cancel/i })).toBeInTheDocument(); }); + }); - it('selecting a channel should remove it from excluded', async () => { - await wrapper.setData({ excluded: excluded.concat(results) }); - wrapper.vm.selected = [results[0]]; - expect(wrapper.vm.excluded).toEqual(excluded.concat([results[1]])); - expect(wrapper.vm.selected).toEqual([results[0]]); - }); + it('exits selection when cancel button is clicked', async () => { + const user = userEvent.setup(); + renderComponent(); - it('deselecting a channel should add it to excluded', () => { - wrapper.vm.selected = [results[0]]; - expect(wrapper.vm.excluded).toEqual(excluded.concat([results[1]])); - expect(wrapper.vm.selected).toEqual([results[0]]); - }); - }); + const selectButton = await waitFor(() => screen.getByTestId('select')); + await user.click(selectButton); + const cancelButton = await waitFor(() => screen.getByRole('button', { name: /cancel/i })); - describe('download csv', () => { - let downloadChannelsCSV; - const excluded = ['item-1', 'item-2']; + await user.click(cancelButton); - beforeEach(async () => { - await wrapper.setData({ selecting: true, excluded }); - downloadChannelsCSV = jest.spyOn(wrapper.vm, 'downloadChannelsCSV'); - downloadChannelsCSV.mockImplementation(() => Promise.resolve()); + await waitFor(() => { + expect(screen.queryByRole('checkbox', { name: /select all/i })).not.toBeInTheDocument(); + expect(screen.queryByText(/channels selected/i)).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument(); }); + }); + }); - it('clicking download CSV should call downloadCSV', async () => { - mocks.downloadCSV.mockImplementationOnce(() => Promise.resolve()); - await wrapper.findComponent('[data-test="download-button"]').trigger('click'); - const menuOptions = wrapper.findAll('.ui-menu-option-content'); - await menuOptions.at(1).trigger('click'); - expect(mocks.downloadCSV).toHaveBeenCalled(); - }); + describe('search', () => { + it('triggers searchCatalog when query parameters change', async () => { + const { router, mockSearchCatalog } = renderComponent(); - it('clicking download PDF should call downloadPDF', async () => { - mocks.downloadPDF.mockImplementationOnce(() => Promise.resolve()); - await wrapper.findComponent('[data-test="download-button"]').trigger('click'); - const menuOptions = wrapper.findAll('.ui-menu-option-content'); - await menuOptions.at(0).trigger('click'); - expect(mocks.downloadPDF).toHaveBeenCalled(); - }); + await waitFor(() => screen.getByText(/results found/i)); - it('downloadCSV should call downloadChannelsCSV with current parameters', async () => { - const keywords = 'Download csv keywords test'; - router.replace({ query: { keywords } }); - await wrapper.vm.downloadCSV(); - expect(downloadChannelsCSV.mock.calls[0][0].keywords).toBe(keywords); - }); + const initialCalls = mockSearchCatalog.mock.calls.length; - it('downloadCSV should call downloadChannelsCSV with list of excluded items', async () => { - await wrapper.vm.downloadCSV(); - expect(downloadChannelsCSV.mock.calls[0][0].excluded).toEqual(excluded); + await router.push({ + name: RouteNames.CATALOG_ITEMS, + query: { keywords: 'search test' }, }); - it('downloadCSV should exit selection mode', async () => { - await wrapper.vm.downloadCSV(); - expect(wrapper.vm.selecting).toBe(false); + await waitFor(() => { + expect(mockSearchCatalog.mock.calls.length).toBeGreaterThan(initialCalls); }); }); });