diff --git a/webapp/channels/src/components/drafts/drafts_link/__snapshots__/drafts_link.test.tsx.snap b/webapp/channels/src/components/drafts/drafts_link/__snapshots__/drafts_link.test.tsx.snap
deleted file mode 100644
index e9da093d4d..0000000000
--- a/webapp/channels/src/components/drafts/drafts_link/__snapshots__/drafts_link.test.tsx.snap
+++ /dev/null
@@ -1,37 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`components/drafts/drafts_link should match snapshot 1`] = `
-
-
-
-`;
diff --git a/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx b/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx
index 08382c7bff..769365d1da 100644
--- a/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx
+++ b/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx
@@ -1,23 +1,241 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {shallow} from 'enzyme';
import React from 'react';
-import {Provider} from 'react-redux';
+import {MemoryRouter, Route} from 'react-router-dom';
-import mockStore from 'tests/test_store';
+import type {DeepPartial} from '@mattermost/types/utilities';
+
+import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
+import {SCHEDULED_POST_URL_SUFFIX} from 'utils/constants';
+import {TestHelper} from 'utils/test_helper';
+
+import type {GlobalState} from 'types/store';
import DraftsLink from './drafts_link';
-describe('components/drafts/drafts_link', () => {
- it('should match snapshot', () => {
- const store = mockStore();
+// Mock the actions that are dispatched
+jest.mock('actions/views/drafts', () => ({
+ getDrafts: jest.fn(() => ({type: 'MOCK_GET_DRAFTS'})),
+}));
- const wrapper = shallow(
-
+jest.mock('mattermost-redux/actions/scheduled_posts', () => ({
+ fetchTeamScheduledPosts: jest.fn(() => ({type: 'MOCK_FETCH_SCHEDULED_POSTS'})),
+}));
+
+// Base state with all required properties
+const baseState: DeepPartial = {
+ entities: {
+ general: {
+ config: {
+ ScheduledPosts: 'true',
+ },
+ license: {
+ IsLicensed: 'true',
+ },
+ },
+ preferences: {
+ myPreferences: {},
+ },
+ teams: {
+ currentTeamId: 'team1',
+ teams: {
+ team1: TestHelper.getTeamMock({id: 'team1'}),
+ },
+ },
+ users: {
+ currentUserId: 'user1',
+ profiles: {
+ user1: TestHelper.getUserMock({id: 'user1'}),
+ },
+ },
+ scheduledPosts: {},
+ channels: {
+ channels: {
+ channel_id_1: TestHelper.getChannelMock({id: 'channel_id_1', type: 'O'}),
+ },
+ channelsInTeam: {
+ team1: new Set(['channel_id_1']),
+ },
+ myMembers: {
+ channel_id_1: {channel_id: 'channel_id_1', user_id: 'user1'},
+ },
+ },
+ },
+ views: {
+ drafts: {
+ remotes: {},
+ },
+ },
+};
+
+// Helper function to render the component with router
+const renderWithRouter = (state: any, initialEntries = ['/team1/channels/town-square']) => {
+ return renderWithContext(
+
+
- ,
+
+ ,
+ state,
+ );
+};
+
+describe('components/drafts/drafts_link', () => {
+ it('should not render when no drafts or scheduled posts exist', () => {
+ renderWithRouter(baseState);
+
+ expect(screen.queryByText('Drafts')).not.toBeInTheDocument();
+ });
+
+ it('should render when drafts exist', () => {
+ const state: DeepPartial = {
+ ...baseState,
+ storage: {
+ storage: {
+ draft_draft1: {timestamp: new Date(), value: {message: 'Draft message', show: true, channelId: 'channel_id_1'}},
+ },
+ },
+ };
+
+ renderWithRouter(state);
+
+ expect(screen.getByText('Drafts')).toBeInTheDocument();
+ expect(screen.getByTestId('draftIcon')).toBeInTheDocument();
+ });
+
+ it('should render when scheduled posts exist', () => {
+ const state: DeepPartial = {
+ ...baseState,
+ entities: {
+ ...baseState.entities,
+ scheduledPosts: {
+ byTeamId: {
+ team1: ['scheduled_post1', 'scheduled_post2'],
+ },
+ },
+ },
+ };
+
+ renderWithRouter(state);
+
+ expect(screen.getByText('Drafts')).toBeInTheDocument();
+ expect(screen.getByTestId('scheduledPostIcon')).toBeInTheDocument();
+ });
+
+ it('should not show scheduled posts badge when scheduled posts are disabled', () => {
+ const state = {
+ ...baseState,
+ entities: {
+ ...baseState.entities,
+ scheduledPosts: {
+ byTeamId: {
+ team1: ['scheduled_post1', 'scheduled_post2'],
+ },
+ },
+ general: {
+ config: {
+ ScheduledPosts: 'false',
+ },
+ },
+ },
+ };
+
+ renderWithRouter(state);
+
+ expect(screen.queryByTestId('scheduledPostIcon')).not.toBeInTheDocument();
+ });
+
+ it('should not show scheduled posts badge when not licensed', () => {
+ const state = {
+ ...baseState,
+ entities: {
+ ...baseState.entities,
+ scheduledPosts: {
+ byTeamId: {
+ team1: ['scheduled_post1', 'scheduled_post2'],
+ },
+ },
+ general: {
+ license: {
+ IsLicensed: 'false',
+ },
+ },
+ },
+ };
+
+ renderWithRouter(state);
+
+ expect(screen.queryByTestId('scheduledPostIcon')).not.toBeInTheDocument();
+ });
+
+ it('should show error indicator when scheduled posts have errors', () => {
+ const state: DeepPartial = {
+ ...baseState,
+ entities: {
+ ...baseState.entities,
+ scheduledPosts: {
+ byTeamId: {
+ team1: ['scheduled_post1', 'scheduled_post2'],
+ },
+ errorsByTeamId: {
+ team1: ['scheduled_post1'],
+ },
+ },
+ },
+ };
+
+ renderWithRouter(state);
+
+ const badge = screen.getByTestId('scheduledPostIcon').closest('.scheduledPostBadge');
+ expect(badge).toHaveClass('persistent');
+ });
+
+ it('should fetch scheduled posts when component mounts', async () => {
+ const fetchTeamScheduledPosts = require('mattermost-redux/actions/scheduled_posts').fetchTeamScheduledPosts;
+
+ renderWithRouter(baseState);
+
+ await waitFor(() => {
+ expect(fetchTeamScheduledPosts).toHaveBeenCalledWith('team1', true);
+ });
+ });
+
+ it('should be active when on drafts route', () => {
+ const state: DeepPartial = {
+ ...baseState,
+ storage: {
+ storage: {
+ draft_draft1: {timestamp: new Date(), value: {message: 'Draft message', show: true, channelId: 'channel_id_1'}},
+ },
+ },
+ };
+
+ renderWithRouter(
+ state,
+ ['/team1/drafts'],
);
- expect(wrapper).toMatchSnapshot();
+
+ const navLink = screen.getByText('Drafts').closest('a');
+ expect(navLink).toHaveClass('active');
+ });
+
+ it('should be active when on scheduled posts route', () => {
+ const state: DeepPartial = {
+ ...baseState,
+ storage: {
+ storage: {
+ draft_draft1: {timestamp: new Date(), value: {message: 'Draft message', show: true, channelId: 'channel_id_1'}},
+ },
+ },
+ };
+
+ renderWithRouter(
+ state,
+ [`/team1/${SCHEDULED_POST_URL_SUFFIX}`],
+ );
+
+ const navLink = screen.getByText('Drafts').closest('a');
+ expect(navLink).toHaveClass('active');
});
});
diff --git a/webapp/channels/src/components/drafts/drafts_link/drafts_link.tsx b/webapp/channels/src/components/drafts/drafts_link/drafts_link.tsx
index d6713e4eed..3a14b0d50d 100644
--- a/webapp/channels/src/components/drafts/drafts_link/drafts_link.tsx
+++ b/webapp/channels/src/components/drafts/drafts_link/drafts_link.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
-import React, {memo, useEffect, useMemo, useRef} from 'react';
+import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector, useDispatch} from 'react-redux';
import {NavLink, useRouteMatch} from 'react-router-dom';
@@ -63,7 +63,9 @@ function DraftsLink() {
const isDraftUrlMatch = useRouteMatch('/:team/drafts');
const isScheduledPostUrlMatch = useRouteMatch('/:team/' + SCHEDULED_POST_URL_SUFFIX);
- const urlMatches = isDraftUrlMatch || isScheduledPostUrlMatch;
+ const urlMatches = Boolean(isDraftUrlMatch || isScheduledPostUrlMatch);
+
+ const isNavLinkActive = useCallback(() => urlMatches, [urlMatches]);
useEffect(() => {
if (syncedDraftsAllowedAndEnabled) {
@@ -123,6 +125,7 @@ function DraftsLink() {
draggable='false'
className='SidebarLink sidebar-item'
tabIndex={0}
+ isActive={isNavLinkActive}
>