signal-desktop/ts/test-both/state/ducks/preferredReactions_test.ts

456 lines
14 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import * as sinon from 'sinon';
import type { StateType } from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import type { PreferredReactionsStateType } from '../../../state/ducks/preferredReactions';
import {
actions,
getEmptyState,
reducer,
} from '../../../state/ducks/preferredReactions';
describe('preferred reactions duck', () => {
const getEmptyRootState = (): StateType =>
rootReducer(undefined, noopAction());
const getRootState = (
preferredReactions: PreferredReactionsStateType
): StateType => ({
...getEmptyRootState(),
preferredReactions,
});
const stateWithOpenCustomizationModal = {
...getEmptyState(),
customizePreferredReactionsModal: {
draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
originalPreferredReactions: ['💙', '👍', '👎', '😂', '😮', '😢'],
selectedDraftEmojiIndex: undefined,
isSaving: false as const,
hadSaveError: false,
},
};
const stateWithOpenCustomizationModalAndSelectedEmoji = {
...stateWithOpenCustomizationModal,
customizePreferredReactionsModal: {
...stateWithOpenCustomizationModal.customizePreferredReactionsModal,
selectedDraftEmojiIndex: 1,
},
};
let sinonSandbox: sinon.SinonSandbox;
beforeEach(() => {
sinonSandbox = sinon.createSandbox();
});
afterEach(() => {
sinonSandbox.restore();
});
describe('cancelCustomizePreferredReactionsModal', () => {
const { cancelCustomizePreferredReactionsModal } = actions;
it("does nothing if the modal isn't open", () => {
const action = cancelCustomizePreferredReactionsModal();
const result = reducer(getEmptyState(), action);
assert.notProperty(result, 'customizePreferredReactionsModal');
});
it('closes the modal if open', () => {
const action = cancelCustomizePreferredReactionsModal();
const result = reducer(stateWithOpenCustomizationModal, action);
assert.notProperty(result, 'customizePreferredReactionsModal');
});
});
describe('deselectDraftEmoji', () => {
const { deselectDraftEmoji } = actions;
it('is a no-op if the customization modal is not open', () => {
const state = getEmptyState();
const action = deselectDraftEmoji();
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('is a no-op if no emoji is selected', () => {
const action = deselectDraftEmoji();
const result = reducer(stateWithOpenCustomizationModal, action);
assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
);
});
it('deselects a currently-selected emoji', () => {
const action = deselectDraftEmoji();
const result = reducer(
stateWithOpenCustomizationModalAndSelectedEmoji,
action
);
assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
);
});
});
describe('openCustomizePreferredReactionsModal', () => {
const { openCustomizePreferredReactionsModal } = actions;
it('opens the customization modal with defaults if no value was stored', () => {
const emptyRootState = getEmptyRootState();
const rootState = {
...emptyRootState,
items: {
...emptyRootState.items,
skinTone: 5,
},
};
const dispatch = sinon.spy();
openCustomizePreferredReactionsModal()(dispatch, () => rootState, null);
const [action] = dispatch.getCall(0).args;
const result = reducer(rootState.preferredReactions, action);
const expectedEmoji = ['❤️', '👍🏿', '👎🏿', '😂', '😮', '😢'];
assert.deepEqual(result.customizePreferredReactionsModal, {
draftPreferredReactions: expectedEmoji,
originalPreferredReactions: expectedEmoji,
selectedDraftEmojiIndex: undefined,
isSaving: false,
hadSaveError: false,
});
});
it('opens the customization modal with stored values', () => {
const storedPreferredReactionEmoji = ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'];
const emptyRootState = getEmptyRootState();
const state = {
...emptyRootState,
items: {
...emptyRootState.items,
preferredReactionEmoji: storedPreferredReactionEmoji,
},
};
const dispatch = sinon.spy();
openCustomizePreferredReactionsModal()(dispatch, () => state, null);
const [action] = dispatch.getCall(0).args;
const result = reducer(state.preferredReactions, action);
assert.deepEqual(result.customizePreferredReactionsModal, {
draftPreferredReactions: storedPreferredReactionEmoji,
originalPreferredReactions: storedPreferredReactionEmoji,
selectedDraftEmojiIndex: undefined,
isSaving: false,
hadSaveError: false,
});
});
});
describe('replaceSelectedDraftEmoji', () => {
const { replaceSelectedDraftEmoji } = actions;
it('is a no-op if the customization modal is not open', () => {
const state = getEmptyState();
const action = replaceSelectedDraftEmoji('🦈');
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('is a no-op if no emoji is selected', () => {
const action = replaceSelectedDraftEmoji('💅');
const result = reducer(stateWithOpenCustomizationModal, action);
assert.strictEqual(result, stateWithOpenCustomizationModal);
});
it('replaces the selected draft emoji and deselects', () => {
const action = replaceSelectedDraftEmoji('🐱');
const result = reducer(
stateWithOpenCustomizationModalAndSelectedEmoji,
action
);
assert.deepStrictEqual(
result.customizePreferredReactionsModal?.draftPreferredReactions,
['✨', '🐱', '🎇', '🦈', '💖', '🅿️']
);
assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
);
});
});
describe('resetDraftEmoji', () => {
const { resetDraftEmoji } = actions;
function getAction(rootState: Readonly<StateType>) {
const dispatch = sinon.spy();
resetDraftEmoji()(dispatch, () => rootState, null);
const [action] = dispatch.getCall(0).args;
return action;
}
it('is a no-op if the customization modal is not open', () => {
const rootState = getEmptyRootState();
const state = rootState.preferredReactions;
const action = getAction(rootState);
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('resets the draft emoji to the defaults', () => {
const rootState = getRootState(stateWithOpenCustomizationModal);
const action = getAction(rootState);
const result = reducer(rootState.preferredReactions, action);
assert.deepEqual(
result.customizePreferredReactionsModal?.draftPreferredReactions,
['❤️', '👍', '👎', '😂', '😮', '😢']
);
});
it('deselects any selected emoji', () => {
const rootState = getRootState(
stateWithOpenCustomizationModalAndSelectedEmoji
);
const action = getAction(rootState);
const result = reducer(rootState.preferredReactions, action);
assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
);
});
});
describe('savePreferredReactions', () => {
const { savePreferredReactions } = actions;
// We want to create a fake ConversationController for testing purposes, and we need
// to sidestep typechecking to do that.
/* eslint-disable @typescript-eslint/no-explicit-any */
let storagePutStub: sinon.SinonStub;
let captureChangeStub: sinon.SinonStub;
let oldConversationController: any;
beforeEach(() => {
storagePutStub = sinonSandbox.stub(window.storage, 'put').resolves();
oldConversationController = window.ConversationController;
captureChangeStub = sinonSandbox.stub();
window.ConversationController = {
getOurConversationOrThrow: (): any => ({
captureChange: captureChangeStub,
}),
} as any;
});
afterEach(() => {
window.ConversationController = oldConversationController;
});
describe('thunk', () => {
it('saves the preferred reaction emoji to local storage', async () => {
await savePreferredReactions()(
sinon.spy(),
() => getRootState(stateWithOpenCustomizationModal),
null
);
sinon.assert.calledWith(
storagePutStub,
'preferredReactionEmoji',
stateWithOpenCustomizationModal.customizePreferredReactionsModal
.draftPreferredReactions
);
});
it('on success, enqueues a storage service upload', async () => {
await savePreferredReactions()(
sinon.spy(),
() => getRootState(stateWithOpenCustomizationModal),
null
);
sinon.assert.calledOnce(captureChangeStub);
});
it('on success, dispatches a pending action followed by a fulfilled action', async () => {
const dispatch = sinon.spy();
await savePreferredReactions()(
dispatch,
() => getRootState(stateWithOpenCustomizationModal),
null
);
sinon.assert.calledTwice(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_PENDING',
});
sinon.assert.calledWith(dispatch, {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_FULFILLED',
});
});
it('on failure, dispatches a pending action followed by a rejected action', async () => {
storagePutStub.rejects(new Error('something went wrong'));
const dispatch = sinon.spy();
await savePreferredReactions()(
dispatch,
() => getRootState(stateWithOpenCustomizationModal),
null
);
sinon.assert.calledTwice(dispatch);
sinon.assert.calledWith(dispatch, {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_PENDING',
});
sinon.assert.calledWith(dispatch, {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_REJECTED',
});
});
it('on failure, does not enqueue a storage service upload', async () => {
storagePutStub.rejects(new Error('something went wrong'));
await savePreferredReactions()(
sinon.spy(),
() => getRootState(stateWithOpenCustomizationModal),
null
);
sinon.assert.notCalled(captureChangeStub);
});
});
describe('SAVE_PREFERRED_REACTIONS_FULFILLED', () => {
const action = {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_FULFILLED' as const,
};
it("does nothing if the modal isn't open", () => {
const result = reducer(getEmptyState(), action);
assert.notProperty(result, 'customizePreferredReactionsModal');
});
it('closes the modal if open', () => {
const result = reducer(stateWithOpenCustomizationModal, action);
assert.notProperty(result, 'customizePreferredReactionsModal');
});
});
describe('SAVE_PREFERRED_REACTIONS_PENDING', () => {
const action = {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_PENDING' as const,
};
it('marks the modal as "saving"', () => {
const result = reducer(stateWithOpenCustomizationModal, action);
assert.isTrue(result.customizePreferredReactionsModal?.isSaving);
});
it('clears any previous errors', () => {
const state = {
...stateWithOpenCustomizationModal,
customizePreferredReactionsModal: {
...stateWithOpenCustomizationModal.customizePreferredReactionsModal,
hadSaveError: true,
},
};
const result = reducer(state, action);
assert.isFalse(result.customizePreferredReactionsModal?.hadSaveError);
});
it('deselects any selected emoji', () => {
const result = reducer(
stateWithOpenCustomizationModalAndSelectedEmoji,
action
);
assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
);
});
});
describe('SAVE_PREFERRED_REACTIONS_REJECTED', () => {
const action = {
type: 'preferredReactions/SAVE_PREFERRED_REACTIONS_REJECTED' as const,
};
it("does nothing if the modal isn't open", () => {
const state = getEmptyState();
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('stops loading', () => {
const result = reducer(stateWithOpenCustomizationModal, action);
assert.isFalse(result.customizePreferredReactionsModal?.isSaving);
});
it('saves that there was an error', () => {
const result = reducer(stateWithOpenCustomizationModal, action);
assert.isTrue(result.customizePreferredReactionsModal?.hadSaveError);
});
});
});
describe('selectDraftEmojiToBeReplaced', () => {
const { selectDraftEmojiToBeReplaced } = actions;
it('is a no-op if the customization modal is not open', () => {
const state = getEmptyState();
const action = selectDraftEmojiToBeReplaced(2);
const result = reducer(state, action);
assert.strictEqual(result, state);
});
it('is a no-op if the index is out of range', () => {
const action = selectDraftEmojiToBeReplaced(99);
const result = reducer(stateWithOpenCustomizationModal, action);
assert.strictEqual(result, stateWithOpenCustomizationModal);
});
it('sets the index as the selected one', () => {
const action = selectDraftEmojiToBeReplaced(3);
const result = reducer(stateWithOpenCustomizationModal, action);
assert.strictEqual(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex,
3
);
});
});
});