diff --git a/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue b/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue index 3268f55f25..0455683633 100644 --- a/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue +++ b/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue @@ -26,8 +26,14 @@ - - - diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/__tests__/useCommunityLibraryUpdates.spec.js b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/__tests__/useCommunityLibraryUpdates.spec.js index 237c81207f..02caf4b841 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/__tests__/useCommunityLibraryUpdates.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/__tests__/useCommunityLibraryUpdates.spec.js @@ -4,8 +4,6 @@ import { CommunityLibraryStatus, NotificationType } from 'shared/constants'; import { CommunityLibrarySubmission } from 'shared/data/resources'; -jest.mock('shared/composables/useSnackbar'); - jest.mock('shared/data/resources', () => ({ CommunityLibrarySubmission: { fetchCollection: jest.fn(), diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js index 432e429638..296f65034a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js @@ -1,10 +1,10 @@ import { ref, computed, unref } from 'vue'; import pickBy from 'lodash/pickBy'; +import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; import { useFetch } from 'shared/composables/useFetch'; import { CommunityLibrarySubmission } from 'shared/data/resources'; import { CommunityLibraryStatus, NotificationType } from 'shared/constants'; import { commonStrings } from 'shared/strings/commonStrings'; -import useSnackbar from 'shared/composables/useSnackbar'; const MAX_RESULTS_PER_PAGE = 10; @@ -55,7 +55,7 @@ const statusToNotificationType = { export default function useCommunityLibraryUpdates({ queryParams } = {}) { const moreObject = ref(null); const isLoadingMore = ref(false); - const { createSnackbar } = useSnackbar(); + const { createSnackbar } = useKSnackbar(); /** * Community Library Submissions are objects that may represent two types of updates: * 1. Creation of a new submission (status: PENDING or SUPERSEDED) @@ -175,7 +175,12 @@ export default function useCommunityLibraryUpdates({ queryParams } = {}) { } catch (error) { const returnedResults = wasLoadingMore ? submissionsUpdates.value : []; isLoadingMore.value = false; - createSnackbar(commonStrings.genericErrorMessage$()); + createSnackbar({ + text: commonStrings.genericErrorMessage$(), + duration: 6000, + autoDismiss: true, + announce: true, + }); // Do not manage any error state in the useFetch composable, just return the current results return returnedResults; } diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue index 442a806055..6255130311 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue @@ -80,6 +80,7 @@ import { useRoute, useRouter } from 'vue-router/composables'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; + import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; import NotificationFilters from './NotificationFilters.vue'; import NotificationList from './NotificationList.vue'; import useCommunityLibraryUpdates from './composables/useCommunityLibraryUpdates'; @@ -90,7 +91,6 @@ import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; import { Modals } from 'shared/constants'; import useStore from 'shared/composables/useStore'; - import useSnackbar from 'shared/composables/useSnackbar'; const NotificationsTab = { UNREAD: 0, @@ -102,7 +102,7 @@ const router = useRouter(); const route = useRoute(); const store = useStore(); - const { createSnackbar } = useSnackbar(); + const { createSnackbar } = useKSnackbar(); const previousQuery = ref(null); const isSaving = ref(false); @@ -175,7 +175,12 @@ await store.dispatch('markNotificationsRead', timestamp.toISOString()); } } catch (error) { - createSnackbar(commonStrings.genericErrorMessage$()); + createSnackbar({ + text: commonStrings.genericErrorMessage$(), + duration: 6000, + autoDismiss: true, + announce: true, + }); } finally { isSaving.value = false; } diff --git a/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js b/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js index 5999031f12..e133ab255a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js @@ -2,6 +2,14 @@ import { render, fireEvent, screen } from '@testing-library/vue'; import VueRouter from 'vue-router'; import StudioCopyToken from '../index.vue'; +const mockCreateSnackbar = jest.fn(); +jest.mock('kolibri-design-system/lib/composables/useKSnackbar', () => ({ + __esModule: true, + default: () => ({ + createSnackbar: mockCreateSnackbar, + }), +})); + function makeWrapper(props = {}) { const mockStore = { dispatch: jest.fn(), @@ -68,10 +76,10 @@ describe('StudioCopyToken', () => { Object.assign(navigator, { clipboard: { writeText }, }); - const { mockStore } = makeWrapper(); + makeWrapper(); const button = screen.getByRole('button'); await fireEvent.click(button); - expect(mockStore.dispatch).toHaveBeenCalledWith('showSnackbar', { text: 'copiedTokenId' }); + expect(mockCreateSnackbar).toHaveBeenCalled(); }); it('dispatches snackbar on failed copy', async () => { @@ -79,10 +87,10 @@ describe('StudioCopyToken', () => { Object.assign(navigator, { clipboard: { writeText }, }); - const { mockStore } = makeWrapper(); + makeWrapper(); const button = screen.getByRole('button'); await fireEvent.click(button); - expect(mockStore.dispatch).toHaveBeenCalledWith('showSnackbar', { text: 'copyFailed' }); + expect(mockCreateSnackbar).toHaveBeenCalled(); }); it('renders the copy button', () => { diff --git a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js new file mode 100644 index 0000000000..88d18be340 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js @@ -0,0 +1,76 @@ +import { mount } from '@vue/test-utils'; +import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; +import GlobalSnackbar from '../GlobalSnackbar.vue'; + +describe('GlobalSnackbar', () => { + let actionCallback; + let hideCallback; + + beforeEach(() => { + actionCallback = jest.fn(); + hideCallback = jest.fn(); + const { clearSnackbar } = useKSnackbar(); + clearSnackbar(); // ensure clean state + }); + + function makeWrapper() { + return mount(GlobalSnackbar, { + stubs: { + KSnackbar: true, + }, + }); + } + + it('renders the snackbar text', async () => { + const { createSnackbar } = useKSnackbar(); + createSnackbar({ text: 'Test Snackbar', announce: true }); + const wrapper = makeWrapper(); + await wrapper.vm.$nextTick(); + expect(wrapper.findComponent({ name: 'KSnackbar' }).props('text')).toBe('Test Snackbar'); + }); + + it('renders the snackbar action correctly', async () => { + const { createSnackbar } = useKSnackbar(); + createSnackbar({ text: 'Test Snackbar', actionText: 'Action', announce: true }); + const wrapper = makeWrapper(); + await wrapper.vm.$nextTick(); + expect(wrapper.findComponent({ name: 'KSnackbar' }).props('actionText')).toBe('Action'); + }); + + it('clicking the action calls the action callback and closes the snackbar', async () => { + const { createSnackbar, snackbarIsVisible } = useKSnackbar(); + createSnackbar({ + text: 'Test Snackbar', + actionText: 'Action', + actionCallback, + hideCallback, + announce: true, + }); + const wrapper = makeWrapper(); + await wrapper.vm.$nextTick(); + + // Simulate action click on the stub + wrapper.findComponent({ name: 'KSnackbar' }).vm.$emit('actionClick'); + + expect(actionCallback).toHaveBeenCalledTimes(1); + expect(hideCallback).toHaveBeenCalledTimes(1); + expect(snackbarIsVisible.value).toBe(false); + }); + + it('closing the snackbar calls hideCallback exactly once when provided', async () => { + const { createSnackbar, snackbarIsVisible } = useKSnackbar(); + createSnackbar({ + text: 'Test Snackbar', + hideCallback, + announce: true, + }); + + const wrapper = makeWrapper(); + await wrapper.vm.$nextTick(); + + wrapper.findComponent({ name: 'KSnackbar' }).vm.$emit('close'); + + expect(hideCallback).toHaveBeenCalledTimes(1); + expect(snackbarIsVisible.value).toBe(false); + }); +}); diff --git a/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue index 2a451c6d61..f80462fab4 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue +++ b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue @@ -144,6 +144,7 @@