mirror of
https://github.com/bitwarden/browser
synced 2024-12-12 09:17:47 +01:00
5e11cb212d
* Introduce Combined State * Cleanup Test * Update Fakes * Address PR Feedback * Update libs/common/src/platform/state/implementations/default-active-user-state.ts Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Prettier * Get rid of ReplaySubject reference --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
import { Observable, ReplaySubject, firstValueFrom, map, timeout } from "rxjs";
|
|
|
|
import { DerivedState, GlobalState, SingleUserState, ActiveUserState } from "../src/platform/state";
|
|
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
|
import { StateUpdateOptions } from "../src/platform/state/state-update-options";
|
|
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
|
import { CombinedState, UserState, activeMarker } from "../src/platform/state/user-state";
|
|
import { UserId } from "../src/types/guid";
|
|
|
|
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
|
shouldUpdate: () => true,
|
|
combineLatestWith: null,
|
|
msTimeout: 10,
|
|
};
|
|
|
|
function populateOptionsWithDefault(
|
|
options: StateUpdateOptions<any, any>,
|
|
): StateUpdateOptions<any, any> {
|
|
return {
|
|
...DEFAULT_TEST_OPTIONS,
|
|
...options,
|
|
};
|
|
}
|
|
|
|
export class FakeGlobalState<T> implements GlobalState<T> {
|
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
|
stateSubject = new ReplaySubject<T>(1);
|
|
|
|
update: <TCombine>(
|
|
configureState: (state: T, dependency: TCombine) => T,
|
|
options?: StateUpdateOptions<T, TCombine>,
|
|
) => Promise<T> = jest.fn(async (configureState, options) => {
|
|
options = populateOptionsWithDefault(options);
|
|
if (this.stateSubject["_buffer"].length == 0) {
|
|
// throw a more helpful not initialized error
|
|
throw new Error(
|
|
"You must initialize the state with a value before calling update. Try calling `stateSubject.next(initialState)` before calling update",
|
|
);
|
|
}
|
|
const current = await firstValueFrom(this.state$.pipe(timeout(100)));
|
|
const combinedDependencies =
|
|
options.combineLatestWith != null
|
|
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
|
: null;
|
|
if (!options.shouldUpdate(current, combinedDependencies)) {
|
|
return current;
|
|
}
|
|
const newState = configureState(current, combinedDependencies);
|
|
this.stateSubject.next(newState);
|
|
return newState;
|
|
});
|
|
|
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
|
|
|
get state$() {
|
|
return this.stateSubject.asObservable();
|
|
}
|
|
}
|
|
|
|
abstract class FakeUserState<T> implements UserState<T> {
|
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
|
stateSubject = new ReplaySubject<CombinedState<T>>(1);
|
|
|
|
protected userId: UserId;
|
|
|
|
state$: Observable<T>;
|
|
combinedState$: Observable<CombinedState<T>>;
|
|
|
|
constructor() {
|
|
this.combinedState$ = this.stateSubject.asObservable();
|
|
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
|
}
|
|
|
|
update: <TCombine>(
|
|
configureState: (state: T, dependency: TCombine) => T,
|
|
options?: StateUpdateOptions<T, TCombine>,
|
|
) => Promise<T> = jest.fn(async (configureState, options) => {
|
|
options = populateOptionsWithDefault(options);
|
|
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
|
const combinedDependencies =
|
|
options.combineLatestWith != null
|
|
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
|
: null;
|
|
if (!options.shouldUpdate(current, combinedDependencies)) {
|
|
return current;
|
|
}
|
|
const newState = configureState(current, combinedDependencies);
|
|
this.stateSubject.next([this.userId, newState]);
|
|
return newState;
|
|
});
|
|
|
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
|
}
|
|
|
|
export class FakeSingleUserState<T> extends FakeUserState<T> implements SingleUserState<T> {
|
|
constructor(readonly userId: UserId) {
|
|
super();
|
|
this.userId = userId;
|
|
}
|
|
}
|
|
export class FakeActiveUserState<T> extends FakeUserState<T> implements ActiveUserState<T> {
|
|
[activeMarker]: true;
|
|
changeActiveUser(userId: UserId) {
|
|
this.userId = userId;
|
|
}
|
|
}
|
|
|
|
export class FakeDerivedState<T> implements DerivedState<T> {
|
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
|
stateSubject = new ReplaySubject<T>(1);
|
|
|
|
forceValue(value: T): Promise<T> {
|
|
this.stateSubject.next(value);
|
|
return Promise.resolve(value);
|
|
}
|
|
forceValueMock = this.forceValue as jest.MockedFunction<typeof this.forceValue>;
|
|
|
|
get state$() {
|
|
return this.stateSubject.asObservable();
|
|
}
|
|
}
|