diff --git a/app/soapbox/__fixtures__/announcements.json b/app/soapbox/__fixtures__/announcements.json
new file mode 100644
index 000000000..20e1960d0
--- /dev/null
+++ b/app/soapbox/__fixtures__/announcements.json
@@ -0,0 +1,44 @@
+[
+ {
+ "id": "1",
+ "content": "
Updated to Soapbox v3.
",
+ "starts_at": null,
+ "ends_at": null,
+ "all_day": false,
+ "published_at": "2022-06-15T18:47:14.190Z",
+ "updated_at": "2022-06-15T18:47:18.339Z",
+ "read": true,
+ "mentions": [],
+ "statuses": [],
+ "tags": [],
+ "emojis": [],
+ "reactions": [
+ {
+ "name": "📈",
+ "count": 476,
+ "me": true
+ }
+ ]
+ },
+ {
+ "id": "2",
+ "content": "Rolled back to Soapbox v2 for now.
",
+ "starts_at": null,
+ "ends_at": null,
+ "all_day": false,
+ "published_at": "2022-07-13T11:11:50.628Z",
+ "updated_at": "2022-07-13T11:11:50.628Z",
+ "read": true,
+ "mentions": [],
+ "statuses": [],
+ "tags": [],
+ "emojis": [],
+ "reactions": [
+ {
+ "name": "📉",
+ "count": 420,
+ "me": false
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/app/soapbox/actions/__tests__/announcements.test.ts b/app/soapbox/actions/__tests__/announcements.test.ts
new file mode 100644
index 000000000..978311585
--- /dev/null
+++ b/app/soapbox/actions/__tests__/announcements.test.ts
@@ -0,0 +1,113 @@
+import { List as ImmutableList } from 'immutable';
+
+import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
+import { __stub } from 'soapbox/api';
+import { mockStore, rootState } from 'soapbox/jest/test-helpers';
+import { normalizeAnnouncement, normalizeInstance } from 'soapbox/normalizers';
+
+import type { APIEntity } from 'soapbox/types/entities';
+
+const announcements = require('soapbox/__fixtures__/announcements.json');
+
+describe('fetchAnnouncements()', () => {
+ describe('with a successful API request', () => {
+ it('should fetch announcements from the API', async() => {
+ const state = rootState
+ .set('instance', normalizeInstance({ version: '3.5.3' }));
+ const store = mockStore(state);
+
+ __stub((mock) => {
+ mock.onGet('/api/v1/announcements').reply(200, announcements);
+ });
+
+ const expectedActions = [
+ { type: 'ANNOUNCEMENTS_FETCH_REQUEST', skipLoading: true },
+ { type: 'ANNOUNCEMENTS_FETCH_SUCCESS', announcements, skipLoading: true },
+ { type: 'POLLS_IMPORT', polls: [] },
+ { type: 'ACCOUNTS_IMPORT', accounts: [] },
+ { type: 'STATUSES_IMPORT', statuses: [], expandSpoilers: false },
+ ];
+ await store.dispatch(fetchAnnouncements());
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('dismissAnnouncement', () => {
+ describe('with a successful API request', () => {
+ it('should mark announcement as dismissed', async() => {
+ const store = mockStore(rootState);
+
+ __stub((mock) => {
+ mock.onPost('/api/v1/announcements/1/dismiss').reply(200);
+ });
+
+ const expectedActions = [
+ { type: 'ANNOUNCEMENTS_DISMISS_REQUEST', id: '1' },
+ { type: 'ANNOUNCEMENTS_DISMISS_SUCCESS', id: '1' },
+ ];
+ await store.dispatch(dismissAnnouncement('1'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('addReaction', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ const state = rootState
+ .setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
+ .setIn(['announcements', 'isLoading'], false);
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ it('should add reaction to a post', async() => {
+ __stub((mock) => {
+ mock.onPut('/api/v1/announcements/2/reactions/📉').reply(200);
+ });
+
+ const expectedActions = [
+ { type: 'ANNOUNCEMENTS_REACTION_ADD_REQUEST', id: '2', name: '📉', skipLoading: true },
+ { type: 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS', id: '2', name: '📉', skipLoading: true },
+ ];
+ await store.dispatch(addReaction('2', '📉'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
+
+describe('removeReaction', () => {
+ let store: ReturnType;
+
+ beforeEach(() => {
+ const state = rootState
+ .setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
+ .setIn(['announcements', 'isLoading'], false);
+ store = mockStore(state);
+ });
+
+ describe('with a successful API request', () => {
+ it('should remove reaction from a post', async() => {
+ __stub((mock) => {
+ mock.onDelete('/api/v1/announcements/2/reactions/📉').reply(200);
+ });
+
+ const expectedActions = [
+ { type: 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST', id: '2', name: '📉', skipLoading: true },
+ { type: 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS', id: '2', name: '📉', skipLoading: true },
+ ];
+ await store.dispatch(removeReaction('2', '📉'));
+ const actions = store.getActions();
+
+ expect(actions).toEqual(expectedActions);
+ });
+ });
+});
diff --git a/app/soapbox/actions/announcements.ts b/app/soapbox/actions/announcements.ts
index 7047f6004..410de3cd9 100644
--- a/app/soapbox/actions/announcements.ts
+++ b/app/soapbox/actions/announcements.ts
@@ -77,7 +77,7 @@ export const dismissAnnouncement = (announcementId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(dismissAnnouncementRequest(announcementId));
- api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
+ return api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
dispatch(dismissAnnouncementSuccess(announcementId));
}).catch(error => {
dispatch(dismissAnnouncementFail(announcementId, error));
@@ -108,6 +108,7 @@ export const addReaction = (announcementId: string, name: string) =>
if (announcement) {
const reaction = announcement.reactions.find(x => x.name === name);
+
if (reaction && reaction.me) {
alreadyAdded = true;
}
@@ -117,7 +118,7 @@ export const addReaction = (announcementId: string, name: string) =>
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
}
- api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
+ return api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
}).catch(err => {
if (!alreadyAdded) {
@@ -152,7 +153,7 @@ export const removeReaction = (announcementId: string, name: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(removeReactionRequest(announcementId, name));
- api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
+ return api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
dispatch(removeReactionSuccess(announcementId, name));
}).catch(err => {
dispatch(removeReactionFail(announcementId, name, err));
diff --git a/app/soapbox/reducers/__tests__/announcements.test.ts b/app/soapbox/reducers/__tests__/announcements.test.ts
new file mode 100644
index 000000000..2051f75d8
--- /dev/null
+++ b/app/soapbox/reducers/__tests__/announcements.test.ts
@@ -0,0 +1,42 @@
+import { List as ImmutableList, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
+
+import {
+ ANNOUNCEMENTS_FETCH_SUCCESS,
+ ANNOUNCEMENTS_UPDATE,
+} from 'soapbox/actions/announcements';
+
+import reducer from '../announcements';
+
+const announcements = require('soapbox/__fixtures__/announcements.json');
+
+describe('accounts reducer', () => {
+ it('should return the initial state', () => {
+ expect(reducer(undefined, {} as any)).toMatchObject({
+ items: ImmutableList(),
+ isLoading: false,
+ show: false,
+ unread: ImmutableSet(),
+ });
+ });
+
+ describe('ANNOUNCEMENTS_FETCH_SUCCESS', () => {
+ it('parses announcements as Records', () => {
+ const action = { type: ANNOUNCEMENTS_FETCH_SUCCESS, announcements };
+ const result = reducer(undefined, action).items;
+
+ expect(result.every((announcement) => ImmutableRecord.isRecord(announcement))).toBe(true);
+ });
+ });
+
+ describe('ANNOUNCEMENTS_UPDATE', () => {
+ it('updates announcements', () => {
+ const state = reducer(undefined, { type: ANNOUNCEMENTS_FETCH_SUCCESS, announcements: [announcements[0]] });
+
+ const action = { type: ANNOUNCEMENTS_UPDATE, announcement: { ...announcements[0], content: 'Updated to Soapbox v3.0.0.
' } };
+ const result = reducer(state, action).items;
+
+ expect(result.size === 1);
+ expect(result.first()?.content === 'Updated to Soapbox v3.0.0.
');
+ });
+ });
+});
diff --git a/app/soapbox/reducers/announcements.ts b/app/soapbox/reducers/announcements.ts
index ab52e0f01..e6be16d2a 100644
--- a/app/soapbox/reducers/announcements.ts
+++ b/app/soapbox/reducers/announcements.ts
@@ -14,8 +14,7 @@ import {
ANNOUNCEMENTS_DELETE,
ANNOUNCEMENTS_DISMISS_SUCCESS,
} from 'soapbox/actions/announcements';
-import { normalizeAnnouncementReaction } from 'soapbox/normalizers';
-import { normalizeAnnouncement } from 'soapbox/normalizers/announcement';
+import { normalizeAnnouncement, normalizeAnnouncementReaction } from 'soapbox/normalizers';
import type { AnyAction } from 'redux';
import type{ Announcement, AnnouncementReaction, APIEntity } from 'soapbox/types/entities';