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 @@