From 892fda1f3bf78491ead35a4804e3580ba98edeb6 Mon Sep 17 00:00:00 2001 From: Monier Ayman Date: Thu, 17 Sep 2026 20:40:26 +0300 Subject: [PATCH 1/6] Migrate Studio snackbar to KDS --- .../components/ClipboardChip.vue | 13 +++- .../sidePanels/ReviewSubmissionSidePanel.vue | 26 +++++-- .../pages/Channels/ChannelActionsDropdown.vue | 54 +++++++++++-- .../pages/Channels/ChannelItem.vue | 19 ++++- .../pages/Channels/ChannelTable.vue | 17 +++- .../__tests__/channelActionsDropdown.spec.js | 14 +++- .../pages/Users/EmailUsersDialog.vue | 19 ++++- .../pages/Users/UserActionsDropdown.vue | 41 ++++++++-- .../pages/Users/UserDetails.vue | 16 +++- .../pages/Users/UserStorage.vue | 13 +++- .../administration/pages/Users/UserTable.vue | 27 +++++-- .../Users/__tests__/emailUsersDialog.spec.js | 21 ++--- .../Clipboard/ContentNodeOptions.vue | 22 ++++-- .../components/Clipboard/index.vue | 30 +++++-- .../components/ContentNodeEditListItem.vue | 18 ++++- .../components/ContentNodeOptions.vue | 34 ++++++-- .../QuickEditModal/EditAudienceModal.vue | 15 +++- .../QuickEditModal/EditBooleanMapModal.vue | 13 +++- .../QuickEditModal/EditCompletionModal.vue | 15 +++- .../QuickEditModal/EditLanguageModal.vue | 18 +++-- .../QuickEditModal/EditSourceModal.vue | 15 +++- .../EditTitleDescriptionModal.vue | 15 +++- .../__tests__/EditAudienceModal.spec.js | 12 ++- .../__tests__/EditBooleanMapModal.spec.js | 12 ++- .../__tests__/EditLanguageModal.spec.js | 15 +++- .../__tests__/EditSourceModal.spec.js | 20 ++--- .../EditTitleDescriptionModal.spec.js | 11 ++- .../RelatedResourcesTab.vue | 20 ++++- .../channelEdit/components/ResourcePanel.vue | 10 ++- .../channelEdit/components/move/MoveModal.vue | 15 +++- .../sidePanels/PublishSidePanel.vue | 10 ++- .../index.vue | 27 ++++--- .../channelEdit/pages/AddNextStepsPage.vue | 13 +++- .../pages/AddPreviousStepsPage.vue | 13 +++- .../pages/StagingTreePage/index.spec.js | 10 ++- .../pages/StagingTreePage/index.vue | 22 +++++- .../channelEdit/views/CurrentTopicView.vue | 33 +++++--- .../ImportFromChannels/SavedSearchesModal.vue | 13 +++- .../SearchOrBrowseWindow.vue | 50 ++++++++---- .../ImportFromChannels/SearchResultsList.vue | 13 +++- .../__tests__/SearchOrBrowseWindow.spec.js | 14 +++- .../views/TreeView/TreeViewBase.vue | 13 +++- .../views/files/FileUploadItem.vue | 11 ++- .../views/progress/ProgressModal.vue | 13 +++- .../channelEdit/views/trash/TrashModal.vue | 12 ++- .../views/trash/__tests__/trashModal.spec.js | 13 +++- .../channelList/views/Channel/CatalogList.vue | 17 +++- .../views/Channel/ChannelInvitation.vue | 15 +++- .../channelList/views/Channel/ChannelItem.vue | 20 ++++- .../channelList/views/Channel/ChannelStar.vue | 16 +++- .../__tests__/DeleteChannelModal.spec.js | 16 ++-- .../Channel/DeleteChannelModal/index.vue | 13 +++- .../RemoveChannelFromListModal.spec.js | 16 ++-- .../RemoveChannelFromListModal/index.vue | 13 +++- .../__tests__/StudioMyChannels.spec.js | 10 ++- .../__tests__/StudioStarredChannels.spec.js | 10 ++- .../__tests__/StudioViewOnlyChannels.spec.js | 10 ++- .../channelList/views/ChannelListIndex.vue | 11 ++- .../views/ChannelSet/ChannelSetModal.vue | 12 ++- .../ChannelSet/StudioCollectionsTable.vue | 18 ++++- .../__tests__/StudioCollectionsTable.spec.js | 4 +- .../__tests__/channelSetModal.spec.js | 4 - .../pages/Account/ChangePasswordForm.vue | 20 ++++- .../settings/pages/Account/FullNameForm.vue | 19 ++++- .../frontend/settings/pages/Account/index.vue | 12 ++- .../settings/pages/Storage/RequestForm.vue | 18 ++++- .../settings/pages/__tests__/account.spec.js | 15 +++- .../contentcuration/frontend/shared/app.js | 2 - .../composables/__mocks__/useSnackbar.js | 26 ------- .../shared/composables/useSnackbar.js | 30 ------- .../frontend/shared/composables/useToken.js | 24 +++++- .../frontend/shared/views/CopyToken.vue | 20 ++++- .../frontend/shared/views/GlobalSnackbar.vue | 78 +++++++------------ .../useCommunityLibraryUpdates.spec.js | 2 - .../composables/useCommunityLibraryUpdates.js | 10 ++- .../shared/views/NotificationsModal/index.vue | 10 ++- .../__tests__/studioCopyToken.spec.js | 13 +++- .../shared/views/channel/ChannelModal.vue | 12 ++- .../shared/views/channel/ChannelSharing.vue | 12 ++- .../views/channel/ChannelSharingTable.vue | 36 +++++++-- .../__tests__/channelSharingTable.spec.js | 14 +++- .../views/errors/TechnicalTextBlock.vue | 18 ++++- .../frontend/shared/vuex/baseStore.js | 2 - .../frontend/shared/vuex/snackbar/index.js | 70 ----------------- 84 files changed, 1109 insertions(+), 429 deletions(-) delete mode 100644 contentcuration/contentcuration/frontend/shared/composables/__mocks__/useSnackbar.js delete mode 100644 contentcuration/contentcuration/frontend/shared/composables/useSnackbar.js delete mode 100644 contentcuration/contentcuration/frontend/shared/vuex/snackbar/index.js 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..c085332e6b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js @@ -4,7 +4,7 @@ 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'; +import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; 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,11 @@ export default function useCommunityLibraryUpdates({ queryParams } = {}) { } catch (error) { const returnedResults = wasLoadingMore ? submissionsUpdates.value : []; isLoadingMore.value = false; - createSnackbar(commonStrings.genericErrorMessage$()); + createSnackbar({ + text: commonStrings.genericErrorMessage$(), + 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..ef30f5c854 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue @@ -90,7 +90,7 @@ import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; import { Modals } from 'shared/constants'; import useStore from 'shared/composables/useStore'; - import useSnackbar from 'shared/composables/useSnackbar'; + import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; 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,11 @@ await store.dispatch('markNotificationsRead', timestamp.toISOString()); } } catch (error) { - createSnackbar(commonStrings.genericErrorMessage$()); + createSnackbar({ + text: commonStrings.genericErrorMessage$(), + 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..0411dda20a 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,15 @@ 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(), @@ -71,7 +80,7 @@ describe('StudioCopyToken', () => { const { mockStore } = 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 () => { @@ -82,7 +91,7 @@ describe('StudioCopyToken', () => { const { mockStore } = 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/channel/ChannelModal.vue b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue index 2a451c6d61..96f04d71c1 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue +++ b/contentcuration/contentcuration/frontend/shared/views/channel/ChannelModal.vue @@ -144,6 +144,7 @@ diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue index 3cf26deccf..6255130311 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/index.vue @@ -177,6 +177,7 @@ } catch (error) { createSnackbar({ text: commonStrings.genericErrorMessage$(), + duration: 6000, autoDismiss: true, announce: true, }); 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..49dc2c683a --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js @@ -0,0 +1,61 @@ + +import { mount } from "@vue/test-utils"; +import GlobalSnackbar from "../GlobalSnackbar.vue"; +import KSnackbar from "kolibri-design-system/lib/KSnackbar/KSnackbar.vue"; +import useKSnackbar from "kolibri-design-system/lib/composables/useKSnackbar"; + +jest.mock("kolibri-design-system/lib/composables/useKSnackbar"); + +describe("GlobalSnackbar", () => { + let mockClearSnackbar; + let mockSnackbarOptions; + let mockSnackbarIsVisible; + + beforeEach(() => { + mockClearSnackbar = jest.fn(); + mockSnackbarOptions = { + text: "Test Snackbar", + actionText: "Action", + actionCallback: jest.fn(), + hideCallback: jest.fn(), + }; + mockSnackbarIsVisible = true; + + useKSnackbar.mockReturnValue({ + snackbarIsVisible: mockSnackbarIsVisible, + snackbarOptions: mockSnackbarOptions, + clearSnackbar: mockClearSnackbar, + }); + }); + + function makeWrapper() { + return mount(GlobalSnackbar, {}); + } + + it("renders the snackbar text", () => { + const wrapper = makeWrapper(); + expect(wrapper.text()).toContain("Test Snackbar"); + }); + + it("renders the snackbar action correctly", () => { + const wrapper = makeWrapper(); + expect(wrapper.text()).toContain("Action"); + }); + + it("clicking the action calls the action callback and closes the snackbar", async () => { + const wrapper = makeWrapper(); + wrapper.findComponent(KSnackbar).vm.$emit("actionClick"); + + expect(mockSnackbarOptions.actionCallback).toHaveBeenCalled(); + expect(mockClearSnackbar).toHaveBeenCalled(); + }); + + it("closing the snackbar calls hideCallback when provided", async () => { + const wrapper = makeWrapper(); + wrapper.findComponent(KSnackbar).vm.$emit("close"); + + expect(mockSnackbarOptions.hideCallback).toHaveBeenCalled(); + expect(mockClearSnackbar).toHaveBeenCalled(); + }); +}); + From f5c91a0242464cf749fccd91268dd9c8e68d2f10 Mon Sep 17 00:00:00 2001 From: Monier Ayman Date: Fri, 2 Oct 2026 18:49:14 +0300 Subject: [PATCH 4/6] Fix snackbar review feedback --- .../__tests__/EditSourceModal.spec.js | 27 ++---- .../frontend/shared/views/GlobalSnackbar.vue | 18 +--- .../composables/useCommunityLibraryUpdates.js | 1 + .../__tests__/studioCopyToken.spec.js | 4 +- .../views/__tests__/GlobalSnackbar.spec.js | 87 +++++++++++-------- 5 files changed, 66 insertions(+), 71 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js index b9781d0ae0..86a161ac25 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js @@ -258,35 +258,26 @@ describe('EditSourceModal', () => { expect(contentNodeActions.updateContentNode).not.toHaveBeenCalled(); }); - test('should show a snackbar with the correct number of edited nodes on success submit', () => { + test('should show a snackbar with the correct number of edited nodes on success submit', async () => { const wrapper = makeWrapper(['node1', 'node2']); - wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); + await wrapper.vm.handleSave(); - const animationFrameId = requestAnimationFrame(() => { - expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Edited attribution for 2 resources' })); - cancelAnimationFrame(animationFrameId); - }); + expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Changes saved' })); }); - test('should show a snack bar with the correct number of edited nodes on success submit if some nodes are imported', () => { + test('should show a snack bar with the correct number of edited nodes on success submit if some nodes are imported', async () => { nodes['node1'].original_source_node_id = 'original_node1'; const wrapper = makeWrapper(['node1', 'node2']); - wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); + await wrapper.vm.handleSave(); - const animationFrameId = requestAnimationFrame(() => { - expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Edited attribution for 1 resource' })); - cancelAnimationFrame(animationFrameId); - }); + expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Changes saved' })); }); - test('should emit close event on success submit', () => { + test('should emit close event on success submit', async () => { const wrapper = makeWrapper(['node1', 'node2']); - wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); + await wrapper.vm.handleSave(); - const animationFrameId = requestAnimationFrame(() => { - expect(wrapper.emitted('close')).toBeTruthy(); - cancelAnimationFrame(animationFrameId); - }); + expect(wrapper.emitted('close')).toBeTruthy(); }); }); diff --git a/contentcuration/contentcuration/frontend/shared/views/GlobalSnackbar.vue b/contentcuration/contentcuration/frontend/shared/views/GlobalSnackbar.vue index 82fb3f7607..be1801b1ce 100644 --- a/contentcuration/contentcuration/frontend/shared/views/GlobalSnackbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/GlobalSnackbar.vue @@ -11,8 +11,8 @@ :autofocus="snackbarOptions.autofocus" :announce="snackbarOptions.announce" :assertive="snackbarOptions.assertive" - @close="handleClose" - @actionClick="handleActionClick" + @close="clearSnackbar" + @actionClick="snackbarOptions.actionCallback && snackbarOptions.actionCallback($event)" @blur="snackbarOptions.onBlur && snackbarOptions.onBlur($event)" /> @@ -38,20 +38,6 @@ clearSnackbar, }; }, - methods: { - handleClose() { - if (this.snackbarOptions && this.snackbarOptions.hideCallback) { - this.snackbarOptions.hideCallback(); - } - this.clearSnackbar(); - }, - handleActionClick(event) { - if (this.snackbarOptions && this.snackbarOptions.actionCallback) { - this.snackbarOptions.actionCallback(event); - } - this.clearSnackbar(); - }, - }, }; diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js index a5f4fb37dd..296f65034a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js @@ -177,6 +177,7 @@ export default function useCommunityLibraryUpdates({ queryParams } = {}) { isLoadingMore.value = false; createSnackbar({ text: commonStrings.genericErrorMessage$(), + duration: 6000, autoDismiss: true, announce: true, }); 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 6b02aa9904..e133ab255a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/StudioCopyToken/__tests__/studioCopyToken.spec.js @@ -76,7 +76,7 @@ describe('StudioCopyToken', () => { Object.assign(navigator, { clipboard: { writeText }, }); - const { mockStore } = makeWrapper(); + makeWrapper(); const button = screen.getByRole('button'); await fireEvent.click(button); expect(mockCreateSnackbar).toHaveBeenCalled(); @@ -87,7 +87,7 @@ describe('StudioCopyToken', () => { Object.assign(navigator, { clipboard: { writeText }, }); - const { mockStore } = makeWrapper(); + makeWrapper(); const button = screen.getByRole('button'); await fireEvent.click(button); expect(mockCreateSnackbar).toHaveBeenCalled(); diff --git a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js index 49dc2c683a..27cd4fc0e3 100644 --- a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js @@ -1,61 +1,78 @@ import { mount } from "@vue/test-utils"; -import GlobalSnackbar from "../GlobalSnackbar.vue"; -import KSnackbar from "kolibri-design-system/lib/KSnackbar/KSnackbar.vue"; import useKSnackbar from "kolibri-design-system/lib/composables/useKSnackbar"; - -jest.mock("kolibri-design-system/lib/composables/useKSnackbar"); +import GlobalSnackbar from "../GlobalSnackbar.vue"; describe("GlobalSnackbar", () => { - let mockClearSnackbar; - let mockSnackbarOptions; - let mockSnackbarIsVisible; + let actionCallback; + let hideCallback; beforeEach(() => { - mockClearSnackbar = jest.fn(); - mockSnackbarOptions = { - text: "Test Snackbar", - actionText: "Action", - actionCallback: jest.fn(), - hideCallback: jest.fn(), - }; - mockSnackbarIsVisible = true; - - useKSnackbar.mockReturnValue({ - snackbarIsVisible: mockSnackbarIsVisible, - snackbarOptions: mockSnackbarOptions, - clearSnackbar: mockClearSnackbar, - }); + actionCallback = jest.fn(); + hideCallback = jest.fn(); + const { clearSnackbar } = useKSnackbar(); + clearSnackbar(); // ensure clean state }); function makeWrapper() { - return mount(GlobalSnackbar, {}); + return mount(GlobalSnackbar, { + stubs: { + KSnackbar: true + } + }); } - it("renders the snackbar text", () => { + it("renders the snackbar text", async () => { + const { createSnackbar } = useKSnackbar(); + createSnackbar({ text: "Test Snackbar", announce: true }); const wrapper = makeWrapper(); - expect(wrapper.text()).toContain("Test Snackbar"); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.snackbarOptions.text).toBe("Test Snackbar"); }); - it("renders the snackbar action correctly", () => { + it("renders the snackbar action correctly", async () => { + const { createSnackbar } = useKSnackbar(); + createSnackbar({ text: "Test Snackbar", actionText: "Action", announce: true }); const wrapper = makeWrapper(); - expect(wrapper.text()).toContain("Action"); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.snackbarOptions.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(); - wrapper.findComponent(KSnackbar).vm.$emit("actionClick"); - - expect(mockSnackbarOptions.actionCallback).toHaveBeenCalled(); - expect(mockClearSnackbar).toHaveBeenCalled(); + 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 when provided", async () => { + 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(); - wrapper.findComponent(KSnackbar).vm.$emit("close"); - - expect(mockSnackbarOptions.hideCallback).toHaveBeenCalled(); - expect(mockClearSnackbar).toHaveBeenCalled(); + await wrapper.vm.$nextTick(); + + wrapper.findComponent({ name: "KSnackbar" }).vm.$emit("close"); + + expect(hideCallback).toHaveBeenCalledTimes(1); + expect(snackbarIsVisible.value).toBe(false); }); }); From b9295a3792442189184531ec691f5e4b56cd2444 Mon Sep 17 00:00:00 2001 From: Monier Ayman Date: Fri, 2 Oct 2026 19:10:29 +0300 Subject: [PATCH 5/6] Fix snackbar test review feedback --- .../__tests__/EditSourceModal.spec.js | 17 ++++++----------- .../views/__tests__/GlobalSnackbar.spec.js | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js index 86a161ac25..1652e708ea 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { Store } from 'vuex'; +import flushPromises from 'flush-promises'; import EditSourceModal from '../EditSourceModal'; import { LicensesList } from 'shared/leUtils/Licenses'; import { constantsTranslationMixin } from 'shared/mixins'; @@ -258,24 +259,18 @@ describe('EditSourceModal', () => { expect(contentNodeActions.updateContentNode).not.toHaveBeenCalled(); }); - test('should show a snackbar with the correct number of edited nodes on success submit', async () => { + test('should show a snackbar with "Changes saved" on success submit', async () => { const wrapper = makeWrapper(['node1', 'node2']); - await wrapper.vm.handleSave(); - - expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Changes saved' })); - }); - - test('should show a snack bar with the correct number of edited nodes on success submit if some nodes are imported', async () => { - nodes['node1'].original_source_node_id = 'original_node1'; - const wrapper = makeWrapper(['node1', 'node2']); - await wrapper.vm.handleSave(); + wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); + await flushPromises(); expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Changes saved' })); }); test('should emit close event on success submit', async () => { const wrapper = makeWrapper(['node1', 'node2']); - await wrapper.vm.handleSave(); + wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); + await flushPromises(); expect(wrapper.emitted('close')).toBeTruthy(); }); diff --git a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js index 27cd4fc0e3..b98154381a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js @@ -27,7 +27,7 @@ describe("GlobalSnackbar", () => { createSnackbar({ text: "Test Snackbar", announce: true }); const wrapper = makeWrapper(); await wrapper.vm.$nextTick(); - expect(wrapper.vm.snackbarOptions.text).toBe("Test Snackbar"); + expect(wrapper.findComponent({ name: "KSnackbar" }).props("text")).toBe("Test Snackbar"); }); it("renders the snackbar action correctly", async () => { @@ -35,7 +35,7 @@ describe("GlobalSnackbar", () => { createSnackbar({ text: "Test Snackbar", actionText: "Action", announce: true }); const wrapper = makeWrapper(); await wrapper.vm.$nextTick(); - expect(wrapper.vm.snackbarOptions.actionText).toBe("Action"); + expect(wrapper.findComponent({ name: "KSnackbar" }).props("actionText")).toBe("Action"); }); it("clicking the action calls the action callback and closes the snackbar", async () => { From fdf4611522bfd9525f2a52963476c0865f3a70a4 Mon Sep 17 00:00:00 2001 From: Monier Ayman Date: Mon, 5 Oct 2026 17:30:17 +0300 Subject: [PATCH 6/6] Fix frontend formatting --- .../__tests__/EditSourceModal.spec.js | 4 +- .../views/__tests__/GlobalSnackbar.spec.js | 40 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js index 1652e708ea..e829aed369 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/QuickEditModal/__tests__/EditSourceModal.spec.js @@ -264,7 +264,9 @@ describe('EditSourceModal', () => { wrapper.find('[data-test="edit-source-modal"]').vm.$emit('submit'); await flushPromises(); - expect(mockCreateSnackbar).toHaveBeenCalledWith(expect.objectContaining({ text: 'Changes saved' })); + expect(mockCreateSnackbar).toHaveBeenCalledWith( + expect.objectContaining({ text: 'Changes saved' }), + ); }); test('should emit close event on success submit', async () => { diff --git a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js index b98154381a..88d18be340 100644 --- a/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/__tests__/GlobalSnackbar.spec.js @@ -1,9 +1,8 @@ +import { mount } from '@vue/test-utils'; +import useKSnackbar from 'kolibri-design-system/lib/composables/useKSnackbar'; +import GlobalSnackbar from '../GlobalSnackbar.vue'; -import { mount } from "@vue/test-utils"; -import useKSnackbar from "kolibri-design-system/lib/composables/useKSnackbar"; -import GlobalSnackbar from "../GlobalSnackbar.vue"; - -describe("GlobalSnackbar", () => { +describe('GlobalSnackbar', () => { let actionCallback; let hideCallback; @@ -17,32 +16,32 @@ describe("GlobalSnackbar", () => { function makeWrapper() { return mount(GlobalSnackbar, { stubs: { - KSnackbar: true - } + KSnackbar: true, + }, }); } - it("renders the snackbar text", async () => { + it('renders the snackbar text', async () => { const { createSnackbar } = useKSnackbar(); - createSnackbar({ text: "Test Snackbar", announce: true }); + createSnackbar({ text: 'Test Snackbar', announce: true }); const wrapper = makeWrapper(); await wrapper.vm.$nextTick(); - expect(wrapper.findComponent({ name: "KSnackbar" }).props("text")).toBe("Test Snackbar"); + expect(wrapper.findComponent({ name: 'KSnackbar' }).props('text')).toBe('Test Snackbar'); }); - it("renders the snackbar action correctly", async () => { + it('renders the snackbar action correctly', async () => { const { createSnackbar } = useKSnackbar(); - createSnackbar({ text: "Test Snackbar", actionText: "Action", announce: true }); + createSnackbar({ text: 'Test Snackbar', actionText: 'Action', announce: true }); const wrapper = makeWrapper(); await wrapper.vm.$nextTick(); - expect(wrapper.findComponent({ name: "KSnackbar" }).props("actionText")).toBe("Action"); + expect(wrapper.findComponent({ name: 'KSnackbar' }).props('actionText')).toBe('Action'); }); - it("clicking the action calls the action callback and closes the snackbar", async () => { + it('clicking the action calls the action callback and closes the snackbar', async () => { const { createSnackbar, snackbarIsVisible } = useKSnackbar(); createSnackbar({ - text: "Test Snackbar", - actionText: "Action", + text: 'Test Snackbar', + actionText: 'Action', actionCallback, hideCallback, announce: true, @@ -51,17 +50,17 @@ describe("GlobalSnackbar", () => { await wrapper.vm.$nextTick(); // Simulate action click on the stub - wrapper.findComponent({ name: "KSnackbar" }).vm.$emit("actionClick"); + 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 () => { + it('closing the snackbar calls hideCallback exactly once when provided', async () => { const { createSnackbar, snackbarIsVisible } = useKSnackbar(); createSnackbar({ - text: "Test Snackbar", + text: 'Test Snackbar', hideCallback, announce: true, }); @@ -69,10 +68,9 @@ describe("GlobalSnackbar", () => { const wrapper = makeWrapper(); await wrapper.vm.$nextTick(); - wrapper.findComponent({ name: "KSnackbar" }).vm.$emit("close"); + wrapper.findComponent({ name: 'KSnackbar' }).vm.$emit('close'); expect(hideCallback).toHaveBeenCalledTimes(1); expect(snackbarIsVisible.value).toBe(false); }); }); -