mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
15 KiB
432 lines
15 KiB
import { skip } from 'rxjs/operators';
|
|
import { ObservableStore, stateFunc } from '../observable-store';
|
|
import { StateWithPropertyChanges } from '../interfaces';
|
|
import { MockStore, UserStore, MockState, getUser, MockUser } from './mocks';
|
|
|
|
let mockStore: any = null;
|
|
let userStore: any = null;
|
|
|
|
beforeEach(() => {
|
|
ObservableStore['isTesting'] = true;
|
|
mockStore = new MockStore({ trackStateHistory: true });
|
|
userStore = new UserStore(null);
|
|
// Clear all existing store state
|
|
ObservableStore.clearState(null);
|
|
});
|
|
|
|
describe('Observable Store', () => {
|
|
|
|
describe('Changing state', () => {
|
|
|
|
it('should change a single property', () => {
|
|
mockStore.updateProp1('test');
|
|
expect(mockStore.currentState.prop1).toEqual('test');
|
|
});
|
|
|
|
it('should reset the store state', () => {
|
|
let newState = { prop1: 'reset prop1 state', prop2: null, user: { name: 'reset user state' }, users: null };
|
|
mockStore.updateProp1('test');
|
|
ObservableStore.resetState(newState);
|
|
expect(mockStore.currentState.prop1).toEqual('reset prop1 state');
|
|
expect(mockStore.currentState.user.name).toEqual('reset user state');
|
|
expect(userStore.currentState.prop1).toEqual('reset prop1 state');
|
|
expect(userStore.currentState.user.name).toEqual('reset user state');
|
|
});
|
|
|
|
it('should clear the store state', () => {
|
|
let newState = { prop1: 'reset prop1 state', prop2: null, user: 'reset user state', users: null };
|
|
mockStore.updateProp1('test');
|
|
ObservableStore.resetState(newState);
|
|
ObservableStore.clearState();
|
|
expect(mockStore.currentState).toBeNull();
|
|
expect(userStore.currentState).toBeNull();
|
|
});
|
|
|
|
it('should execute an anonymous function', () => {
|
|
const capitalizeProp1: stateFunc<MockState> = (state: MockState) => {
|
|
state.prop1 = state.prop1.toLocaleUpperCase();
|
|
return state;
|
|
};
|
|
|
|
const capitalizeSpy = jasmine.createSpy().and.callFake(capitalizeProp1);
|
|
mockStore.updateProp1('test');
|
|
mockStore.updateUsingAFunction(capitalizeSpy);
|
|
|
|
expect(capitalizeSpy).toHaveBeenCalled();
|
|
expect(mockStore.currentState.prop1).toEqual('TEST');
|
|
});
|
|
|
|
it('should execute an anonymous function on a slice of data', () => {
|
|
const updateUser: stateFunc<MockState> = (state: MockState) => {
|
|
if (!state) {
|
|
state = { prop1: null, prop2: null, user: null, users: null };
|
|
}
|
|
state.user = { name: 'fred' };
|
|
return { user: state.user };
|
|
};
|
|
|
|
const updateUserSpy = jasmine.createSpy().and.callFake(updateUser);
|
|
|
|
mockStore.updateUsingAFunction(updateUserSpy);
|
|
|
|
expect(updateUserSpy).toHaveBeenCalled();
|
|
expect(mockStore.currentState.user.name).toEqual('fred');
|
|
});
|
|
});
|
|
|
|
describe('Subscriptions', () => {
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should NOT receive notification if no state has changed', () => {
|
|
let receiveUpdate = false;
|
|
const sub = mockStore.stateChanged.pipe(skip(1)).subscribe(() => (receiveUpdate = true));
|
|
|
|
expect(receiveUpdate).toBeFalsy();
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive notification when state has been changed', () => {
|
|
let receiveUpdate = false;
|
|
const sub = mockStore.stateChanged.pipe(skip(1)).subscribe(() => (receiveUpdate = true));
|
|
|
|
mockStore.updateProp1('test');
|
|
|
|
expect(receiveUpdate).toBeTruthy();
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive notification when state has been reset', () => {
|
|
let receivedUpdate = false;
|
|
let receivedState = null;
|
|
const sub = mockStore.stateChanged.pipe(skip(1)).subscribe((state) => {
|
|
receivedUpdate = true;
|
|
receivedState = state;
|
|
});
|
|
mockStore.updateProp1('initial state');
|
|
ObservableStore.resetState({ prop1: 'state reset', prop2: null, user: null, users: null });
|
|
|
|
expect(receivedUpdate).toBeTruthy();
|
|
expect(receivedState.prop1).toEqual('state reset');
|
|
expect(receivedState.prop2).toBe(null);
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// deprecated
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive state notification when includeStateChangesOnSubscribe set [deprecated]', () => {
|
|
let mockStore = new MockStore({ includeStateChangesOnSubscribe: true });
|
|
let receivedData;
|
|
const sub = mockStore.stateChanged.pipe(skip(1)).subscribe(stateWithChanges => receivedData = stateWithChanges);
|
|
|
|
mockStore.updateProp1('test');
|
|
|
|
expect(receivedData.state.prop1).toEqual('test');
|
|
expect(receivedData.stateChanges.prop1).toEqual('test');
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive notification from stateChangedWithChanges', () => {
|
|
let mockStore = new MockStore({});
|
|
let receivedData: StateWithPropertyChanges<MockState>;
|
|
const sub = mockStore.stateWithPropertyChanges.pipe(skip(1)).subscribe(stateWithChanges => {
|
|
receivedData = stateWithChanges;
|
|
});
|
|
|
|
mockStore.updateProp1('test');
|
|
|
|
expect(receivedData.state.prop1).toEqual('test');
|
|
expect(receivedData.stateChanges.prop1).toEqual('test');
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive notification from globalStateChanged', () => {
|
|
let mockStore = new MockStore({});
|
|
let receivedData = [];
|
|
const sub = mockStore.globalStateChanged.pipe(skip(1)).subscribe(state => {
|
|
receivedData.push(state);
|
|
});
|
|
|
|
mockStore.updateProp1('test');
|
|
expect(receivedData.length).toEqual(1);
|
|
expect(receivedData[0].prop1).toEqual('test');
|
|
sub.unsubscribe();
|
|
});
|
|
|
|
// we will skip 1 to account for the initial BehaviorSubject<T> value
|
|
it('should receive notification from globalStateChangedWithChanges', () => {
|
|
let mockStore = new MockStore({});
|
|
let receivedData: StateWithPropertyChanges<MockState>;
|
|
const sub = mockStore.globalStateWithPropertyChanges.pipe(skip(1)).subscribe(stateWithChanges => {
|
|
receivedData = stateWithChanges;
|
|
});
|
|
|
|
mockStore.updateProp1('test');
|
|
|
|
expect(receivedData.state.prop1).toEqual('test');
|
|
expect(receivedData.stateChanges.prop1).toEqual('test');
|
|
sub.unsubscribe();
|
|
});
|
|
});
|
|
|
|
describe('Action', () => {
|
|
it('should add valid action to stateHistory', () => {
|
|
const mockAction = 'Mock_Action';
|
|
mockStore.updateForTestAction('test', mockAction);
|
|
|
|
expect(mockStore.stateHistory[mockStore.stateHistory.length - 1].action).toEqual(mockAction);
|
|
});
|
|
});
|
|
|
|
describe('getStateProperty', () => {
|
|
|
|
it('should retrieve single user property when string property name passed', () => {
|
|
userStore.setState({ user: getUser() });
|
|
expect(userStore.currentState.user).toBeTruthy();
|
|
let state = userStore.getStateProperty('user');
|
|
expect(state.name).toBeTruthy();
|
|
expect(state.users).toBeUndefined();
|
|
});
|
|
|
|
});
|
|
|
|
describe('getStateSliceProperty', () => {
|
|
|
|
it('should retrieve single user property from slice when string property name passed', () => {
|
|
userStore = new UserStore({
|
|
stateSliceSelector: state => {
|
|
if (state) {
|
|
return { ...state.user };
|
|
}
|
|
}
|
|
});
|
|
userStore.setState({ user: getUser() });
|
|
expect(userStore.currentState).toBeTruthy();
|
|
let userName = userStore.getStateSliceProperty('name');
|
|
expect(userName).toBeTruthy();
|
|
});
|
|
|
|
});
|
|
|
|
describe('SliceSelector', () => {
|
|
|
|
it('should only have MockUser when requesting state', () => {
|
|
userStore = new UserStore({
|
|
stateSliceSelector: state => {
|
|
if (state) {
|
|
return { user: state.user };
|
|
}
|
|
}
|
|
});
|
|
|
|
userStore.updateUser({ name: 'foo', address: { city: 'Phoenix', state: 'AZ', zip: 85349 } });
|
|
|
|
const state = userStore.currentState;
|
|
|
|
expect(state.prop1).toBeFalsy();
|
|
expect(state.prop2).toBeFalsy();
|
|
// although the state is populated, slice will only populate the User
|
|
expect(state.user).toBeTruthy();
|
|
});
|
|
|
|
});
|
|
|
|
describe('Cloning', () => {
|
|
let user = null;
|
|
|
|
beforeEach(() => {
|
|
user = getUser();
|
|
});
|
|
|
|
it('should clone Map object added to store', () => {
|
|
let map = new Map();
|
|
map.set('key1', 22);
|
|
map.set('key2', 32);
|
|
userStore.updateMap(map);
|
|
let state = userStore.getCurrentState();
|
|
expect(map).toBe(map);
|
|
expect(state.map).not.toBe(map);
|
|
expect(state.map.size).toEqual(map.size);
|
|
});
|
|
|
|
it('should deep clone when setState called', () => {
|
|
userStore.updateUser(user);
|
|
user.address.city = 'Las Vegas';
|
|
expect(userStore.currentState.user.address.city).not.toEqual('Las Vegas');
|
|
});
|
|
|
|
it('should NOT deep clone when setState called', () => {
|
|
userStore.updateUser(user, false); // don't clone when setting state
|
|
user.address.city = 'Las Vegas';
|
|
expect(userStore.currentState.user.address.city).toEqual('Las Vegas');
|
|
});
|
|
|
|
it('should NOT deep clone when setState or getState called', () => {
|
|
// Set state but don't clone
|
|
userStore.updateUser(user, false);
|
|
// Get state but don't clone
|
|
let nonClonedUserState = userStore.getCurrentState(false);
|
|
// Update user which should also update store
|
|
nonClonedUserState.user.address.city = 'Las Vegas';
|
|
// Ensure user was updated by reference
|
|
expect(userStore.currentState.user.address.city).toEqual('Las Vegas');
|
|
});
|
|
|
|
it('should deep clone when setState or getState called', () => {
|
|
// Set state but don't clone
|
|
userStore.updateUser(user);
|
|
let clonedUserState = userStore.getCurrentState();
|
|
clonedUserState.user.address.city = 'Las Vegas';
|
|
expect(userStore.currentState.user.address.city).not.toEqual('Las Vegas');
|
|
});
|
|
|
|
it('should deep clone array', () => {
|
|
userStore.addToUsers(user);
|
|
let users = userStore.getCurrentState().users;
|
|
// Should NOT affect store users array since it would be cloned
|
|
users.push({ name: 'user2', address: { city: 'Chandler', state: 'AZ', zip: 85249 } });
|
|
expect(users.length).toEqual(2);
|
|
expect(userStore.currentState.users.length).toEqual(1);
|
|
});
|
|
|
|
it('should NOT deep clone array', () => {
|
|
userStore.addToUsers(user, false);
|
|
let users = userStore.getCurrentState(false).users;
|
|
users.push({ name: 'user2', address: { city: 'Chandler', state: 'AZ', zip: 85249 } });
|
|
expect(userStore.currentState.users.length).toEqual(2);
|
|
});
|
|
|
|
it('should deep clone with matching number of keys', () => {
|
|
userStore.updateUser(user);
|
|
userStore.addToUsers(user);
|
|
const stateKeys = Object.getOwnPropertyNames(userStore.currentState);
|
|
expect(stateKeys.length).toEqual(2);
|
|
});
|
|
|
|
it('should NOT deep clone but have matching number of keys', () => {
|
|
userStore.updateUser(user, false);
|
|
userStore.addToUsers(user, false);
|
|
const stateKeys = Object.getOwnPropertyNames(userStore.currentState);
|
|
expect(stateKeys.length).toEqual(2);
|
|
});
|
|
|
|
});
|
|
|
|
describe('globalSettings', () => {
|
|
|
|
it('should store global settings', () => {
|
|
ObservableStore.globalSettings = { trackStateHistory: true };
|
|
const settingsKeys = Object.getOwnPropertyNames(ObservableStore.globalSettings);
|
|
expect(settingsKeys.length).toEqual(1);
|
|
});
|
|
|
|
it('should error when no global settings passed', () => {
|
|
try {
|
|
ObservableStore.globalSettings = null;
|
|
}
|
|
catch (e) {
|
|
expect(e.message).toEqual('Please provide the global settings you would like to apply to Observable Store');
|
|
}
|
|
});
|
|
|
|
it('should set initial state', () => {
|
|
ObservableStore.initializeState({ user: { name: 'Fred' } });
|
|
const state = userStore.currentState;
|
|
expect(state.user.name).toEqual('Fred');
|
|
});
|
|
|
|
it('should error when setting initial state and state already exists', () => {
|
|
// Update store state
|
|
mockStore.updateProp1();
|
|
try {
|
|
// Try to initialize state (should throw)
|
|
ObservableStore.initializeState({ user: { name: 'Fred' } });
|
|
}
|
|
catch (e) {
|
|
expect(e.message).toEqual('The store state has already been initialized. initializeStoreState() can ' +
|
|
'only be called once BEFORE any store state has been set.');
|
|
}
|
|
|
|
});
|
|
|
|
it('should error when global settings passed more than once', () => {
|
|
ObservableStore.globalSettings = { trackStateHistory: true };
|
|
try {
|
|
ObservableStore.globalSettings = { trackStateHistory: false };
|
|
}
|
|
catch (e) {
|
|
expect(e.message).toEqual('Observable Store global settings may only be set once.');
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
describe('trackHistory', () => {
|
|
|
|
let user = null;
|
|
|
|
beforeEach(() => {
|
|
user = getUser();
|
|
});
|
|
|
|
it('should set trackHistory through global settings', () => {
|
|
ObservableStore.globalSettings = { trackStateHistory: true };
|
|
userStore = new UserStore(null);
|
|
userStore.updateUser(user);
|
|
userStore.updateUser(user);
|
|
expect(userStore.stateHistory.length).toEqual(2);
|
|
});
|
|
|
|
it('should turn off trackHistory through global settings', () => {
|
|
ObservableStore.globalSettings = { trackStateHistory: false };
|
|
userStore = new UserStore(null);
|
|
userStore.updateUser(user);
|
|
userStore.updateUser(user);
|
|
expect(userStore.stateHistory.length).toEqual(0);
|
|
});
|
|
|
|
it('should turn on trackHistory through settings', () => {
|
|
ObservableStore.globalSettings = {};
|
|
userStore = new UserStore({ trackStateHistory: true });
|
|
userStore.updateUser(user);
|
|
userStore.updateUser(user);
|
|
expect(userStore.stateHistory.length).toEqual(2);
|
|
});
|
|
|
|
it('should turn off trackHistory through settings', () => {
|
|
ObservableStore.globalSettings = {};
|
|
userStore = new UserStore({ trackStateHistory: false });
|
|
userStore.updateUser(user);
|
|
expect(userStore.stateHistory.length).toEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('isInitialized', () => {
|
|
it('should return false when ObservableStore state has not yet been initialized', () => {
|
|
expect(ObservableStore.isStoreInitialized).toEqual(false);
|
|
});
|
|
|
|
it('should return true when ObservableStore state has been initialized', () => {
|
|
const state = {
|
|
number: 420,
|
|
awesome: false,
|
|
}
|
|
expect(ObservableStore.isStoreInitialized).toEqual(false);
|
|
ObservableStore.initializeState(state);
|
|
expect(ObservableStore.isStoreInitialized).toEqual(true);
|
|
});
|
|
|
|
it('should return false after the ObservableStore state has been initialized and then reset', () => {
|
|
const state = {
|
|
number: 69,
|
|
awesome: true,
|
|
}
|
|
ObservableStore.initializeState(state);
|
|
ObservableStore.clearState();
|
|
expect(ObservableStore.isStoreInitialized).toEqual(false);
|
|
});
|
|
});
|
|
|
|
});
|
|
|