mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +01: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:
parent
48d161009d
commit
211d7a2626
69
libs/common/spec/fake-account-service.ts
Normal file
69
libs/common/spec/fake-account-service.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { Observable, ReplaySubject } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
||||||
|
import { AuthenticationStatus } from "../src/auth/enums/authentication-status";
|
||||||
|
import { UserId } from "../src/types/guid";
|
||||||
|
|
||||||
|
export function mockAccountServiceWith(
|
||||||
|
userId: UserId,
|
||||||
|
info: Partial<AccountInfo> = {},
|
||||||
|
): FakeAccountService {
|
||||||
|
const fullInfo: AccountInfo = {
|
||||||
|
...info,
|
||||||
|
...{
|
||||||
|
name: "name",
|
||||||
|
email: "email",
|
||||||
|
status: AuthenticationStatus.Locked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const service = new FakeAccountService({ [userId]: fullInfo });
|
||||||
|
service.activeAccountSubject.next({ id: userId, ...fullInfo });
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FakeAccountService implements AccountService {
|
||||||
|
mock = mock<AccountService>();
|
||||||
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||||
|
accountsSubject = new ReplaySubject<Record<UserId, AccountInfo>>(1);
|
||||||
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||||
|
activeAccountSubject = new ReplaySubject<{ id: UserId } & AccountInfo>(1);
|
||||||
|
private _activeUserId: UserId;
|
||||||
|
get activeUserId() {
|
||||||
|
return this._activeUserId;
|
||||||
|
}
|
||||||
|
get accounts$() {
|
||||||
|
return this.accountsSubject.asObservable();
|
||||||
|
}
|
||||||
|
get activeAccount$() {
|
||||||
|
return this.activeAccountSubject.asObservable();
|
||||||
|
}
|
||||||
|
accountLock$: Observable<UserId>;
|
||||||
|
accountLogout$: Observable<UserId>;
|
||||||
|
|
||||||
|
constructor(initialData: Record<UserId, AccountInfo>) {
|
||||||
|
this.accountsSubject.next(initialData);
|
||||||
|
this.activeAccountSubject.subscribe((data) => (this._activeUserId = data?.id));
|
||||||
|
this.activeAccountSubject.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAccount(userId: UserId, accountData: AccountInfo): Promise<void> {
|
||||||
|
this.mock.addAccount(userId, accountData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAccountName(userId: UserId, name: string): Promise<void> {
|
||||||
|
this.mock.setAccountName(userId, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAccountEmail(userId: UserId, email: string): Promise<void> {
|
||||||
|
this.mock.setAccountEmail(userId, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise<void> {
|
||||||
|
this.mock.setAccountStatus(userId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchAccount(userId: UserId): Promise<void> {
|
||||||
|
this.mock.switchAccount(userId);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
import { UserId } from "../src/types/guid";
|
import { UserId } from "../src/types/guid";
|
||||||
import { DerivedStateDependencies } from "../src/types/state";
|
import { DerivedStateDependencies } from "../src/types/state";
|
||||||
|
|
||||||
|
import { FakeAccountService } from "./fake-account-service";
|
||||||
import {
|
import {
|
||||||
FakeActiveUserState,
|
FakeActiveUserState,
|
||||||
FakeDerivedState,
|
FakeDerivedState,
|
||||||
@ -24,56 +26,114 @@ import {
|
|||||||
} from "./fake-state";
|
} from "./fake-state";
|
||||||
|
|
||||||
export class FakeGlobalStateProvider implements GlobalStateProvider {
|
export class FakeGlobalStateProvider implements GlobalStateProvider {
|
||||||
|
mock = mock<GlobalStateProvider>();
|
||||||
|
establishedMocks: Map<string, FakeGlobalState<unknown>> = new Map();
|
||||||
states: Map<string, GlobalState<unknown>> = new Map();
|
states: Map<string, GlobalState<unknown>> = new Map();
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||||
let result = this.states.get(keyDefinition.buildCacheKey("global")) as GlobalState<T>;
|
this.mock.get(keyDefinition);
|
||||||
|
let result = this.states.get(keyDefinition.buildCacheKey("global"));
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
|
let fake: FakeGlobalState<T>;
|
||||||
|
// Look for established mock
|
||||||
|
if (this.establishedMocks.has(keyDefinition.key)) {
|
||||||
|
fake = this.establishedMocks.get(keyDefinition.key) as FakeGlobalState<T>;
|
||||||
|
} else {
|
||||||
|
fake = new FakeGlobalState<T>();
|
||||||
|
}
|
||||||
|
fake.keyDefinition = keyDefinition;
|
||||||
|
result = fake;
|
||||||
|
this.states.set(keyDefinition.buildCacheKey("global"), result);
|
||||||
|
|
||||||
result = new FakeGlobalState<T>();
|
result = new FakeGlobalState<T>();
|
||||||
this.states.set(keyDefinition.buildCacheKey("global"), result);
|
this.states.set(keyDefinition.buildCacheKey("global"), result);
|
||||||
}
|
}
|
||||||
return result;
|
return result as GlobalState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFake<T>(keyDefinition: KeyDefinition<T>): FakeGlobalState<T> {
|
getFake<T>(keyDefinition: KeyDefinition<T>): FakeGlobalState<T> {
|
||||||
return this.get(keyDefinition) as FakeGlobalState<T>;
|
return this.get(keyDefinition) as FakeGlobalState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeGlobalState<T> {
|
||||||
|
if (!this.establishedMocks.has(keyDefinitionKey)) {
|
||||||
|
this.establishedMocks.set(keyDefinitionKey, new FakeGlobalState<T>(initialValue));
|
||||||
|
}
|
||||||
|
return this.establishedMocks.get(keyDefinitionKey) as FakeGlobalState<T>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
||||||
|
mock = mock<SingleUserStateProvider>();
|
||||||
|
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
|
||||||
states: Map<string, SingleUserState<unknown>> = new Map();
|
states: Map<string, SingleUserState<unknown>> = new Map();
|
||||||
get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T> {
|
get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T> {
|
||||||
let result = this.states.get(keyDefinition.buildCacheKey("user", userId)) as SingleUserState<T>;
|
this.mock.get(userId, keyDefinition);
|
||||||
|
let result = this.states.get(keyDefinition.buildCacheKey("user", userId));
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = new FakeSingleUserState<T>(userId);
|
let fake: FakeSingleUserState<T>;
|
||||||
|
// Look for established mock
|
||||||
|
if (this.establishedMocks.has(keyDefinition.key)) {
|
||||||
|
fake = this.establishedMocks.get(keyDefinition.key) as FakeSingleUserState<T>;
|
||||||
|
} else {
|
||||||
|
fake = new FakeSingleUserState<T>(userId);
|
||||||
|
}
|
||||||
|
fake.keyDefinition = keyDefinition;
|
||||||
|
result = fake;
|
||||||
this.states.set(keyDefinition.buildCacheKey("user", userId), result);
|
this.states.set(keyDefinition.buildCacheKey("user", userId), result);
|
||||||
}
|
}
|
||||||
return result;
|
return result as SingleUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFake<T>(userId: UserId, keyDefinition: KeyDefinition<T>): FakeSingleUserState<T> {
|
getFake<T>(userId: UserId, keyDefinition: KeyDefinition<T>): FakeSingleUserState<T> {
|
||||||
return this.get(userId, keyDefinition) as FakeSingleUserState<T>;
|
return this.get(userId, keyDefinition) as FakeSingleUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockFor<T>(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState<T> {
|
||||||
|
if (!this.establishedMocks.has(keyDefinitionKey)) {
|
||||||
|
this.establishedMocks.set(keyDefinitionKey, new FakeSingleUserState<T>(userId, initialValue));
|
||||||
|
}
|
||||||
|
return this.establishedMocks.get(keyDefinitionKey) as FakeSingleUserState<T>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||||
states: Map<string, ActiveUserState<unknown>> = new Map();
|
establishedMocks: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||||
|
|
||||||
|
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||||
|
|
||||||
|
constructor(public accountService: FakeAccountService) {}
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
||||||
let result = this.states.get(
|
let result = this.states.get(keyDefinition.buildCacheKey("user", "active"));
|
||||||
keyDefinition.buildCacheKey("user", "active"),
|
|
||||||
) as ActiveUserState<T>;
|
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = new FakeActiveUserState<T>();
|
// Look for established mock
|
||||||
|
if (this.establishedMocks.has(keyDefinition.key)) {
|
||||||
|
result = this.establishedMocks.get(keyDefinition.key);
|
||||||
|
} else {
|
||||||
|
result = new FakeActiveUserState<T>(this.accountService);
|
||||||
|
}
|
||||||
|
result.keyDefinition = keyDefinition;
|
||||||
this.states.set(keyDefinition.buildCacheKey("user", "active"), result);
|
this.states.set(keyDefinition.buildCacheKey("user", "active"), result);
|
||||||
}
|
}
|
||||||
return result;
|
return result as ActiveUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFake<T>(keyDefinition: KeyDefinition<T>): FakeActiveUserState<T> {
|
getFake<T>(keyDefinition: KeyDefinition<T>): FakeActiveUserState<T> {
|
||||||
return this.get(keyDefinition) as FakeActiveUserState<T>;
|
return this.get(keyDefinition) as FakeActiveUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState<T> {
|
||||||
|
if (!this.establishedMocks.has(keyDefinitionKey)) {
|
||||||
|
this.establishedMocks.set(
|
||||||
|
keyDefinitionKey,
|
||||||
|
new FakeActiveUserState<T>(this.accountService, initialValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.establishedMocks.get(keyDefinitionKey) as FakeActiveUserState<T>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FakeStateProvider implements StateProvider {
|
export class FakeStateProvider implements StateProvider {
|
||||||
@ -97,9 +157,11 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
return this.derived.get(parentState$, deriveDefinition, dependencies);
|
return this.derived.get(parentState$, deriveDefinition, dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(public accountService: FakeAccountService) {}
|
||||||
|
|
||||||
global: FakeGlobalStateProvider = new FakeGlobalStateProvider();
|
global: FakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||||
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
|
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider();
|
||||||
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider();
|
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService);
|
||||||
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { Observable, ReplaySubject, firstValueFrom, map, timeout } from "rxjs";
|
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
|
// 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";
|
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
|
// 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 { UserId } from "../src/types/guid";
|
||||||
|
|
||||||
|
import { FakeAccountService } from "./fake-account-service";
|
||||||
|
|
||||||
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
||||||
shouldUpdate: () => true,
|
shouldUpdate: () => true,
|
||||||
combineLatestWith: null,
|
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
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||||
stateSubject = new ReplaySubject<T>(1);
|
stateSubject = new ReplaySubject<T>(1);
|
||||||
|
|
||||||
|
constructor(initialValue?: T) {
|
||||||
|
this.stateSubject.next(initialValue ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
update: <TCombine>(
|
update: <TCombine>(
|
||||||
configureState: (state: T, dependency: TCombine) => T,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
options?: StateUpdateOptions<T, TCombine>,
|
options?: StateUpdateOptions<T, TCombine>,
|
||||||
@ -47,34 +59,56 @@ export class FakeGlobalState<T> implements GlobalState<T> {
|
|||||||
}
|
}
|
||||||
const newState = configureState(current, combinedDependencies);
|
const newState = configureState(current, combinedDependencies);
|
||||||
this.stateSubject.next(newState);
|
this.stateSubject.next(newState);
|
||||||
|
this.nextMock(newState);
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
nextMock = jest.fn<void, [T]>();
|
||||||
|
|
||||||
get state$() {
|
get state$() {
|
||||||
return this.stateSubject.asObservable();
|
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
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||||
stateSubject = new ReplaySubject<CombinedState<T>>(1);
|
stateSubject = new ReplaySubject<CombinedState<T>>(1);
|
||||||
|
|
||||||
protected userId: UserId;
|
|
||||||
|
|
||||||
state$: Observable<T>;
|
state$: Observable<T>;
|
||||||
combinedState$: Observable<CombinedState<T>>;
|
combinedState$: Observable<CombinedState<T>>;
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
|
readonly userId: UserId,
|
||||||
|
initialValue?: T,
|
||||||
|
) {
|
||||||
|
this.stateSubject.next([userId, initialValue ?? null]);
|
||||||
|
|
||||||
this.combinedState$ = this.stateSubject.asObservable();
|
this.combinedState$ = this.stateSubject.asObservable();
|
||||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
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,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
options?: StateUpdateOptions<T, TCombine>,
|
options?: StateUpdateOptions<T, TCombine>,
|
||||||
) => Promise<T> = jest.fn(async (configureState, options) => {
|
): Promise<T> {
|
||||||
options = populateOptionsWithDefault(options);
|
options = populateOptionsWithDefault(options);
|
||||||
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
||||||
const combinedDependencies =
|
const combinedDependencies =
|
||||||
@ -86,22 +120,87 @@ abstract class FakeUserState<T> implements UserState<T> {
|
|||||||
}
|
}
|
||||||
const newState = configureState(current, combinedDependencies);
|
const newState = configureState(current, combinedDependencies);
|
||||||
this.stateSubject.next([this.userId, newState]);
|
this.stateSubject.next([this.userId, newState]);
|
||||||
|
this.nextMock(newState);
|
||||||
return newState;
|
return newState;
|
||||||
});
|
}
|
||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
}
|
|
||||||
|
|
||||||
export class FakeSingleUserState<T> extends FakeUserState<T> implements SingleUserState<T> {
|
nextMock = jest.fn<void, [T]>();
|
||||||
constructor(readonly userId: UserId) {
|
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||||
super();
|
get keyDefinition() {
|
||||||
this.userId = userId;
|
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;
|
[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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,8 +36,6 @@ describe("accountService", () => {
|
|||||||
sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider);
|
sut = new AccountServiceImplementation(messagingService, logService, globalStateProvider);
|
||||||
|
|
||||||
accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS);
|
accountsState = globalStateProvider.getFake(ACCOUNT_ACCOUNTS);
|
||||||
// initialize to empty
|
|
||||||
accountsState.stateSubject.next({});
|
|
||||||
activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
activeAccountIdState = globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -57,7 +55,10 @@ describe("accountService", () => {
|
|||||||
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) });
|
||||||
activeAccountIdState.stateSubject.next(userId);
|
activeAccountIdState.stateSubject.next(userId);
|
||||||
|
|
||||||
expect(emissions).toEqual([{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) }]);
|
expect(emissions).toEqual([
|
||||||
|
undefined, // initial value
|
||||||
|
{ id: userId, ...userInfo(AuthenticationStatus.Unlocked) },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update the status if the account status changes", async () => {
|
it("should update the status if the account status changes", async () => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
||||||
import {
|
import {
|
||||||
FakeActiveUserStateProvider,
|
FakeActiveUserStateProvider,
|
||||||
FakeDerivedStateProvider,
|
FakeDerivedStateProvider,
|
||||||
@ -19,9 +20,11 @@ describe("DefaultStateProvider", () => {
|
|||||||
let singleUserStateProvider: FakeSingleUserStateProvider;
|
let singleUserStateProvider: FakeSingleUserStateProvider;
|
||||||
let globalStateProvider: FakeGlobalStateProvider;
|
let globalStateProvider: FakeGlobalStateProvider;
|
||||||
let derivedStateProvider: FakeDerivedStateProvider;
|
let derivedStateProvider: FakeDerivedStateProvider;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
activeUserStateProvider = new FakeActiveUserStateProvider();
|
accountService = mockAccountServiceWith("fakeUserId" as UserId);
|
||||||
|
activeUserStateProvider = new FakeActiveUserStateProvider(accountService);
|
||||||
singleUserStateProvider = new FakeSingleUserStateProvider();
|
singleUserStateProvider = new FakeSingleUserStateProvider();
|
||||||
globalStateProvider = new FakeGlobalStateProvider();
|
globalStateProvider = new FakeGlobalStateProvider();
|
||||||
derivedStateProvider = new FakeDerivedStateProvider();
|
derivedStateProvider = new FakeDerivedStateProvider();
|
||||||
|
@ -151,8 +151,8 @@ export class KeyDefinition<T> {
|
|||||||
throw new Error("You must provide a userId when building a user scoped cache key.");
|
throw new Error("You must provide a userId when building a user scoped cache key.");
|
||||||
}
|
}
|
||||||
return userId === null
|
return userId === null
|
||||||
? `${scope}_${userId}_${this.stateDefinition.name}_${this.key}`
|
? `${this.stateDefinition.storageLocation}_${scope}_${userId}_${this.stateDefinition.name}_${this.key}`
|
||||||
: `${scope}_${this.stateDefinition.name}_${this.key}`;
|
: `${this.stateDefinition.storageLocation}_${scope}_${this.stateDefinition.name}_${this.key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get errorKeyName() {
|
private get errorKeyName() {
|
||||||
|
Loading…
Reference in New Issue
Block a user