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';