diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/__tests__/catalogList.spec.js b/contentcuration/contentcuration/frontend/channelList/views/Channel/__tests__/catalogList.spec.js index 32c9de6843..a5118f3ad4 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/__tests__/catalogList.spec.js +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/__tests__/catalogList.spec.js @@ -1,200 +1,245 @@ -import { mount } from '@vue/test-utils'; -import { factory } from '../../../store'; -import router from '../../../router'; -import { RouteNames } from '../../../constants'; +import { render, screen, waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import { createLocalVue } from '@vue/test-utils'; +import Vuex, { Store } from 'vuex'; +import VueRouter from 'vue-router'; import CatalogList from '../CatalogList'; +import { RouteNames } from '../../../constants'; -const store = factory(); - -router.push({ name: RouteNames.CATALOG_ITEMS }); +const localVue = createLocalVue(); +localVue.use(Vuex); +localVue.use(VueRouter); + +const mockChannels = [ + { + id: 'channel-1', + name: 'Channel 1', + description: 'Test channel 1', + language: 'en', + }, + { + id: 'channel-2', + name: 'Channel 2', + description: 'Test channel 2', + language: 'en', + }, +]; const results = ['channel-1', 'channel-2']; -function makeWrapper(computed = {}) { - const loadCatalog = jest.spyOn(CatalogList.methods, 'loadCatalog'); - loadCatalog.mockImplementation(() => Promise.resolve()); - - const downloadCSV = jest.spyOn(CatalogList.methods, 'downloadCSV'); - const downloadPDF = jest.spyOn(CatalogList.methods, 'downloadPDF'); +function makeWrapper(overrides = {}) { + const mockSearchCatalog = jest.fn(() => Promise.resolve()); - const wrapper = mount(CatalogList, { - router, - store, - computed: { - page() { - return { - count: results.length, - results, - }; + const store = new Store({ + state: { + connection: { + online: overrides.offline ? false : true, }, - ...computed, }, - stubs: { - CatalogFilters: true, + getters: { + loggedIn: () => true, + }, + actions: { + showSnackbar: jest.fn(), + }, + modules: { + channel: { + namespaced: true, + state: { + channelsMap: { + 'channel-1': mockChannels[0], + 'channel-2': mockChannels[1], + }, + }, + getters: { + getChannels: state => ids => { + return ids.map(id => state.channelsMap[id]).filter(Boolean); + }, + getChannel: state => id => state.channelsMap[id], + }, + actions: { + getChannelListDetails: jest.fn(() => Promise.resolve(mockChannels)), + }, + }, + channelList: { + namespaced: true, + state: { + page: { + count: results.length, + results, + page_number: 1, + total_pages: 1, + next: null, + previous: null, + ...overrides.page, + }, + }, + actions: { + searchCatalog: mockSearchCatalog, + }, + }, }, }); - return [wrapper, { loadCatalog, downloadCSV, downloadPDF }]; -} - -describe('catalogFilterBar', () => { - let wrapper, mocks; - beforeEach(async () => { - [wrapper, mocks] = makeWrapper(); - await wrapper.setData({ loading: false }); + const router = new VueRouter({ + routes: [ + { + name: RouteNames.CATALOG_ITEMS, + path: '/catalog', + }, + { + name: RouteNames.CATALOG_DETAILS, + path: '/catalog/:channelId', + }, + ], }); - it('should call loadCatalog on mount', () => { - [wrapper, mocks] = makeWrapper(); - expect(mocks.loadCatalog).toHaveBeenCalled(); + router.push({ name: RouteNames.CATALOG_ITEMS }).catch(() => {}); + + const renderResult = render(CatalogList, { + localVue, + store, + router, + stubs: { + CatalogFilters: true, + }, }); - describe('on query change', () => { - const searchCatalogMock = jest.fn(); + return { + ...renderResult, + store, + router, + mockSearchCatalog, + }; +} - beforeEach(() => { - router.replace({ query: {} }).catch(() => {}); - searchCatalogMock.mockReset(); - [wrapper, mocks] = makeWrapper({ - debouncedSearch() { - return searchCatalogMock; - }, - }); - }); +describe('CatalogList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); - it('should call debouncedSearch', async () => { - const keywords = 'search catalog test'; - router.push({ query: { keywords } }).catch(() => {}); - await wrapper.vm.$nextTick(); - expect(searchCatalogMock).toHaveBeenCalled(); + describe('initial load', () => { + it('should render catalog results on mount', async () => { + makeWrapper(); + await waitFor(() => { + // Component renders actual translation - use regex for flexibility + 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('should call searchCatalog on mount', async () => { + const { mockSearchCatalog } = makeWrapper(); + await waitFor(() => { + expect(mockSearchCatalog).toHaveBeenCalled(); + }); }); - 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('should display download button when results are available', async () => { + makeWrapper(); + await waitFor(() => { + // Use actual button text rendered by component + expect(screen.getByText(/download a summary/i)).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); - }); + describe('selection mode workflow', () => { + it('should hide checkboxes and toolbar initially', async () => { + makeWrapper(); + await waitFor(() => screen.getByText(/download a summary/i)); - it('should activate when select button is clicked', async () => { - await wrapper.findComponent('[data-test="select"]').trigger('click'); - expect(wrapper.vm.selecting).toBe(true); - }); + // Toolbar should not be visible initially (appears only in selection mode) + expect(screen.queryByText(/select all/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/cancel/i)).not.toBeInTheDocument(); + }); - 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('should enter selection mode and show toolbar when user clicks select button', async () => { + const user = userEvent.setup(); + makeWrapper(); - 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); + await waitFor(() => screen.getByText(/download a summary/i)); + await user.click(screen.getByText(/download a summary/i)); + + await waitFor(() => { + expect(screen.getByText(/select all/i)).toBeInTheDocument(); + expect(screen.getByText(/cancel/i)).toBeInTheDocument(); + expect(screen.getByText(/channels selected/i)).toBeInTheDocument(); }); }); - describe('selecting channels', () => { - const excluded = ['item-1']; + it('should exit selection mode when user clicks cancel', async () => { + const user = userEvent.setup(); + makeWrapper(); - beforeEach(async () => { - await wrapper.setData({ - selecting: true, - excluded, - }); - }); + // Enter selection mode + await waitFor(() => screen.getByText(/download a summary/i)); + await user.click(screen.getByText(/download a summary/i)); + await waitFor(() => screen.getByText(/cancel/i)); - 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); - }); + await user.click(screen.getByText(/cancel/i)); - 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.queryByText(/cancel/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/select all/i)).not.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]]); - }); + describe('channel selection', () => { + it('should display select-all checkbox in selection mode', async () => { + const user = userEvent.setup(); + makeWrapper(); - 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]]); + await waitFor(() => screen.getByText(/download a summary/i)); + await user.click(screen.getByText(/download a summary/i)); + + await waitFor(() => { + expect(screen.getByText(/select all/i)).toBeInTheDocument(); + expect(screen.getByText(/channels selected/i)).toBeInTheDocument(); }); }); + }); - describe('download csv', () => { - let downloadChannelsCSV; - const excluded = ['item-1', 'item-2']; + describe('search and filtering', () => { + it('should call searchCatalog when query parameters change', async () => { + const { router, mockSearchCatalog } = makeWrapper(); - beforeEach(async () => { - await wrapper.setData({ selecting: true, excluded }); - downloadChannelsCSV = jest.spyOn(wrapper.vm, 'downloadChannelsCSV'); - downloadChannelsCSV.mockImplementation(() => Promise.resolve()); - }); + await waitFor(() => screen.getByText(/results found/i)); + + const initialCalls = mockSearchCatalog.mock.calls.length; - 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(); + await router.push({ + name: RouteNames.CATALOG_ITEMS, + query: { keywords: 'search test' }, }); - 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(() => { + expect(mockSearchCatalog.mock.calls.length).toBeGreaterThan(initialCalls); }); + }); + + it('should maintain results display after filtering', async () => { + const { router } = makeWrapper(); - 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); + await waitFor(() => screen.getByText(/results found/i)); + + await router.push({ + name: RouteNames.CATALOG_ITEMS, + query: { keywords: 'test search' }, }); - 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 waitFor(() => { + expect(screen.getByText(/results found/i)).toBeInTheDocument(); }); + }); + }); + + describe('download workflow', () => { + it('should show select button to enable downloads', async () => { + makeWrapper(); - it('downloadCSV should exit selection mode', async () => { - await wrapper.vm.downloadCSV(); - expect(wrapper.vm.selecting).toBe(false); + await waitFor(() => { + expect(screen.getByText(/download a summary/i)).toBeInTheDocument(); }); }); });