mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
Ps/improve state provider fakers (#7494)
* Expand state provider fakes - default null initial value for fake states - Easier mocking of key definitions through just the use of key names - allows for not exporting KeyDefinition as long as the key doesn't collide - mock of fake state provider to verify `get` calls - `nextMock` for use of the fn mock matchers on emissions of `state$` - `FakeAccountService` which allows for easy initialization and working with account switching * Small bug fix for cache key collision on key definitions unique by only storage location * Fix initial value for test
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import { Observable, ReplaySubject, firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { DerivedState, GlobalState, SingleUserState, ActiveUserState } from "../src/platform/state";
|
||||
import {
|
||||
DerivedState,
|
||||
GlobalState,
|
||||
SingleUserState,
|
||||
ActiveUserState,
|
||||
KeyDefinition,
|
||||
} 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 { CombinedState, activeMarker } from "../src/platform/state/user-state";
|
||||
import { UserId } from "../src/types/guid";
|
||||
|
||||
import { FakeAccountService } from "./fake-account-service";
|
||||
|
||||
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
||||
shouldUpdate: () => true,
|
||||
combineLatestWith: null,
|
||||
@@ -26,6 +34,10 @@ export class FakeGlobalState<T> implements GlobalState<T> {
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<T>(1);
|
||||
|
||||
constructor(initialValue?: T) {
|
||||
this.stateSubject.next(initialValue ?? null);
|
||||
}
|
||||
|
||||
update: <TCombine>(
|
||||
configureState: (state: T, dependency: TCombine) => T,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
@@ -47,34 +59,56 @@ export class FakeGlobalState<T> implements GlobalState<T> {
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.stateSubject.next(newState);
|
||||
this.nextMock(newState);
|
||||
return newState;
|
||||
});
|
||||
|
||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||
nextMock = jest.fn<void, [T]>();
|
||||
|
||||
get state$() {
|
||||
return this.stateSubject.asObservable();
|
||||
}
|
||||
|
||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: KeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FakeUserState<T> implements UserState<T> {
|
||||
export class FakeSingleUserState<T> implements SingleUserState<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() {
|
||||
constructor(
|
||||
readonly userId: UserId,
|
||||
initialValue?: T,
|
||||
) {
|
||||
this.stateSubject.next([userId, initialValue ?? null]);
|
||||
|
||||
this.combinedState$ = this.stateSubject.asObservable();
|
||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
||||
}
|
||||
|
||||
update: <TCombine>(
|
||||
nextState(state: T) {
|
||||
this.stateSubject.next([this.userId, state]);
|
||||
}
|
||||
|
||||
async update<TCombine>(
|
||||
configureState: (state: T, dependency: TCombine) => T,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
) => Promise<T> = jest.fn(async (configureState, options) => {
|
||||
): Promise<T> {
|
||||
options = populateOptionsWithDefault(options);
|
||||
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
||||
const combinedDependencies =
|
||||
@@ -86,22 +120,87 @@ abstract class FakeUserState<T> implements UserState<T> {
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.stateSubject.next([this.userId, newState]);
|
||||
this.nextMock(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;
|
||||
nextMock = jest.fn<void, [T]>();
|
||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: KeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
export class FakeActiveUserState<T> extends FakeUserState<T> implements ActiveUserState<T> {
|
||||
export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
||||
[activeMarker]: true;
|
||||
changeActiveUser(userId: UserId) {
|
||||
this.userId = userId;
|
||||
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<CombinedState<T>>(1);
|
||||
|
||||
state$: Observable<T>;
|
||||
combinedState$: Observable<CombinedState<T>>;
|
||||
|
||||
constructor(
|
||||
private accountService: FakeAccountService,
|
||||
initialValue?: T,
|
||||
) {
|
||||
this.stateSubject.next([accountService.activeUserId, initialValue ?? null]);
|
||||
|
||||
this.combinedState$ = this.stateSubject.asObservable();
|
||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
||||
}
|
||||
|
||||
get userId() {
|
||||
return this.accountService.activeUserId;
|
||||
}
|
||||
|
||||
nextState(state: T) {
|
||||
this.stateSubject.next([this.userId, state]);
|
||||
}
|
||||
|
||||
async update<TCombine>(
|
||||
configureState: (state: T, dependency: TCombine) => T,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
): Promise<T> {
|
||||
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]);
|
||||
this.nextMock(this.userId, newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||
|
||||
nextMock = jest.fn<void, [UserId, T]>();
|
||||
|
||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: KeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user