diff --git a/apps/user_status/src/UserStatus.spec.ts b/apps/user_status/src/UserStatus.spec.ts new file mode 100644 index 0000000000000..0d58fc87fe73c --- /dev/null +++ b/apps/user_status/src/UserStatus.spec.ts @@ -0,0 +1,55 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { emit } from '@nextcloud/event-bus' +import { loadState } from '@nextcloud/initial-state' +import { cleanup, render } from '@testing-library/vue' +import { createPinia } from 'pinia' +import { afterEach, describe, expect, it, vi } from 'vitest' +import UserStatus from './UserStatus.vue' +import { useUserStatusStore } from './store/userStatus.ts' + +vi.mock('@nextcloud/auth') +vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn() })) + +describe('UserStatus', () => { + afterEach(cleanup) + + it('shows the status from the initial state', async () => { + vi.mocked(loadState).mockReturnValue({ + status: 'away', + statusIsUserDefined: true, + message: 'In a meeting', + icon: '📅', + clearAt: null, + messageIsPredefined: false, + messageId: null, + }) + + const component = render(UserStatus, { + props: { inline: true }, + global: { plugins: [createPinia()] }, + }) + + expect(await component.findByRole('button', { name: '📅 In a meeting' })).toBeTruthy() + }) + + it('updates the shared store from status events of the current user', async () => { + vi.mocked(loadState).mockReturnValue({ status: 'online', statusIsUserDefined: true, message: null, icon: null }) + + const pinia = createPinia() + const component = render(UserStatus, { + props: { inline: true }, + global: { plugins: [pinia] }, + }) + expect(await component.findByRole('button', { name: 'Online' })).toBeTruthy() + + emit('user_status:status.updated', { userId: 'other', status: 'dnd', message: 'Other user', icon: null }) + emit('user_status:status.updated', { userId: 'test', status: 'dnd', message: null, icon: null }) + + expect(await component.findByRole('button', { name: 'Do not disturb' })).toBeTruthy() + expect(useUserStatusStore(pinia).status).toBe('dnd') + }) +}) diff --git a/apps/user_status/src/UserStatus.vue b/apps/user_status/src/UserStatus.vue index 789097407288d..5e8687ff7f7a3 100644 --- a/apps/user_status/src/UserStatus.vue +++ b/apps/user_status/src/UserStatus.vue @@ -82,10 +82,10 @@ export default { /** * Loads the current user's status from initial state - * and stores it in Vuex + * and stores it in the user status store */ mounted() { - this.$store.dispatch('loadStatusFromInitialState') + this.userStatusStore.loadStatusFromInitialState() if (OC.config.session_keepalive) { this.stopHeartbeat = startHeartbeat((isAway) => this._backgroundHeartbeat(isAway)) @@ -127,9 +127,9 @@ export default { try { const status = await sendHeartbeat(isAway) if (status?.userId) { - this.$store.dispatch('setStatusFromHeartbeat', status) + this.userStatusStore.loadStatusFromServer(status) } else { - await this.$store.dispatch('reFetchStatusFromServer') + await this.userStatusStore.reFetchStatusFromServer() } } catch (error) { logger.debug('Failed sending heartbeat, got: ' + error.response?.status) @@ -138,7 +138,7 @@ export default { handleUserStatusUpdated(state) { if (getCurrentUser()?.uid === state.userId) { - this.$store.dispatch('setStatusFromObject', { + this.userStatusStore.loadStatusFromServer({ status: state.status, icon: state.icon, message: state.message, diff --git a/apps/user_status/src/components/PredefinedStatusesList.spec.ts b/apps/user_status/src/components/PredefinedStatusesList.spec.ts new file mode 100644 index 0000000000000..aefb837a27154 --- /dev/null +++ b/apps/user_status/src/components/PredefinedStatusesList.spec.ts @@ -0,0 +1,39 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { cleanup, render } from '@testing-library/vue' +import { createPinia } from 'pinia' +import { afterEach, describe, expect, it, vi } from 'vitest' +import PredefinedStatusesList from './PredefinedStatusesList.vue' +import { fetchAllPredefinedStatuses } from '../services/predefinedStatusService.js' +import { useUserStatusStore } from '../store/userStatus.ts' + +vi.mock('../services/predefinedStatusService.js', () => ({ + fetchAllPredefinedStatuses: vi.fn(), +})) + +describe('PredefinedStatusesList', () => { + afterEach(cleanup) + + it('loads the predefined statuses and marks the current one as selected', async () => { + vi.mocked(fetchAllPredefinedStatuses).mockResolvedValue([ + { id: 'meeting', icon: '📅', message: 'In a meeting', clearAt: null }, + { id: 'sick', icon: '🤒', message: 'Out sick', clearAt: null }, + ]) + + const pinia = createPinia() + useUserStatusStore(pinia).loadStatusFromServer({ messageId: 'sick' }) + + const component = render(PredefinedStatusesList, { + global: { plugins: [pinia] }, + }) + + const list = await component.findByRole('list', { name: 'Predefined statuses' }) + expect(list.textContent).toContain('In a meeting') + expect(list.textContent).toContain('Out sick') + expect(fetchAllPredefinedStatuses).toHaveBeenCalledOnce() + expect(component.getByRole('radio', { checked: true, name: /Out sick/ })).toBeTruthy() + }) +}) diff --git a/apps/user_status/src/components/PredefinedStatusesList.vue b/apps/user_status/src/components/PredefinedStatusesList.vue index 068656c21ba25..d7ee19b3314d8 100644 --- a/apps/user_status/src/components/PredefinedStatusesList.vue +++ b/apps/user_status/src/components/PredefinedStatusesList.vue @@ -27,9 +27,11 @@