Skip to content
Open
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Expand Down