sforkowany z mirror/soapbox
Add Onboarding controls to Redux
rodzic
3bd8ef13ef
commit
c8c715ee4b
|
@ -0,0 +1,101 @@
|
||||||
|
import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
|
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
||||||
|
|
||||||
|
describe('checkOnboarding()', () => {
|
||||||
|
let mockGetItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
getItem: (key: string) => mockGetItem(key),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if localStorage item is not set', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if localStorage item is invalid', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue('invalid');
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue('1');
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(checkOnboardingStatus());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||||
|
expect(mockGetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startOnboarding()', () => {
|
||||||
|
let mockSetItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
setItem: (key: string, value: string) => mockSetItem(key, value),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockSetItem = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(startOnboarding());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_START' }]);
|
||||||
|
expect(mockSetItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('endOnboarding()', () => {
|
||||||
|
let mockRemoveItem: any;
|
||||||
|
|
||||||
|
mockWindowProperty('localStorage', {
|
||||||
|
removeItem: (key: string) => mockRemoveItem(key),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRemoveItem = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct action', async() => {
|
||||||
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
|
const store = mockStore(state);
|
||||||
|
|
||||||
|
await store.dispatch(endOnboarding());
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual([{ type: 'ONBOARDING_END' }]);
|
||||||
|
expect(mockRemoveItem.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,8 +0,0 @@
|
||||||
import { changeSetting, saveSettings } from './settings';
|
|
||||||
|
|
||||||
export const INTRODUCTION_VERSION = 20181216044202;
|
|
||||||
|
|
||||||
export const closeOnboarding = () => dispatch => {
|
|
||||||
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
|
|
||||||
dispatch(saveSettings());
|
|
||||||
};
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
const ONBOARDING_START = 'ONBOARDING_START';
|
||||||
|
const ONBOARDING_END = 'ONBOARDING_END';
|
||||||
|
|
||||||
|
const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding';
|
||||||
|
|
||||||
|
type OnboardingStartAction = {
|
||||||
|
type: typeof ONBOARDING_START
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnboardingEndAction = {
|
||||||
|
type: typeof ONBOARDING_END
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingActions = OnboardingStartAction | OnboardingEndAction
|
||||||
|
|
||||||
|
const checkOnboardingStatus = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1';
|
||||||
|
|
||||||
|
if (needsOnboarding) {
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1');
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
};
|
||||||
|
|
||||||
|
const endOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY);
|
||||||
|
dispatch({ type: ONBOARDING_END });
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
ONBOARDING_END,
|
||||||
|
ONBOARDING_START,
|
||||||
|
checkOnboardingStatus,
|
||||||
|
endOnboarding,
|
||||||
|
startOnboarding,
|
||||||
|
};
|
|
@ -63,6 +63,23 @@ const customRender = (
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockWindowProperty = (property: any, value: any) => {
|
||||||
|
const { [property]: originalProperty } = window;
|
||||||
|
delete window[property];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.defineProperty(window, property, {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
window[property] = originalProperty;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export * from '@testing-library/react';
|
export * from '@testing-library/react';
|
||||||
export {
|
export {
|
||||||
customRender as render,
|
customRender as render,
|
||||||
|
@ -70,4 +87,5 @@ export {
|
||||||
applyActions,
|
applyActions,
|
||||||
rootState,
|
rootState,
|
||||||
rootReducer,
|
rootReducer,
|
||||||
|
mockWindowProperty,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ONBOARDING_START, ONBOARDING_END } from 'soapbox/actions/onboarding';
|
||||||
|
|
||||||
|
import reducer from '../onboarding';
|
||||||
|
|
||||||
|
describe('onboarding reducer', () => {
|
||||||
|
it('should return the initial state', () => {
|
||||||
|
expect(reducer(undefined, {})).toEqual({
|
||||||
|
needsOnboarding: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ONBOARDING_START', () => {
|
||||||
|
it('sets "needsOnboarding" to "true"', () => {
|
||||||
|
const initialState = { needsOnboarding: false };
|
||||||
|
const action = { type: ONBOARDING_START };
|
||||||
|
expect(reducer(initialState, action).needsOnboarding).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ONBOARDING_END', () => {
|
||||||
|
it('sets "needsOnboarding" to "false"', () => {
|
||||||
|
const initialState = { needsOnboarding: true };
|
||||||
|
const action = { type: ONBOARDING_END };
|
||||||
|
expect(reducer(initialState, action).needsOnboarding).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -39,6 +39,7 @@ import meta from './meta';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
import onboarding from './onboarding';
|
||||||
import patron from './patron';
|
import patron from './patron';
|
||||||
import pending_statuses from './pending_statuses';
|
import pending_statuses from './pending_statuses';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
|
@ -118,6 +119,7 @@ const reducers = {
|
||||||
accounts_meta,
|
accounts_meta,
|
||||||
trending_statuses,
|
trending_statuses,
|
||||||
verification,
|
verification,
|
||||||
|
onboarding,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a default state from all reducers: it has the key and `undefined`
|
// Build a default state from all reducers: it has the key and `undefined`
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ONBOARDING_START, ONBOARDING_END } from 'soapbox/actions/onboarding';
|
||||||
|
|
||||||
|
import type { OnboardingActions } from 'soapbox/actions/onboarding';
|
||||||
|
|
||||||
|
type OnboardingState = {
|
||||||
|
needsOnboarding: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: OnboardingState = {
|
||||||
|
needsOnboarding: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function onboarding(state: OnboardingState = initialState, action: OnboardingActions): OnboardingState {
|
||||||
|
switch(action.type) {
|
||||||
|
case ONBOARDING_START:
|
||||||
|
return { ...state, needsOnboarding: true };
|
||||||
|
case ONBOARDING_END:
|
||||||
|
return { ...state, needsOnboarding: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue