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 = { shouldUpdate: () => true, combineLatestWith: null, msTimeout: 10, }; function populateOptionsWithDefault( options: StateUpdateOptions, ): StateUpdateOptions { return { ...DEFAULT_TEST_OPTIONS, ...options, }; } export class FakeGlobalState implements GlobalState { // eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup stateSubject = new ReplaySubject(1); update: ( configureState: (state: T, dependency: TCombine) => T, options?: StateUpdateOptions, ) => Promise = 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; get state$() { return this.stateSubject.asObservable(); } } abstract class FakeUserState implements UserState { // eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup stateSubject = new ReplaySubject>(1); protected userId: UserId; state$: Observable; combinedState$: Observable>; constructor() { this.combinedState$ = this.stateSubject.asObservable(); this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state)); } update: ( configureState: (state: T, dependency: TCombine) => T, options?: StateUpdateOptions, ) => Promise = 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; } export class FakeSingleUserState extends FakeUserState implements SingleUserState { constructor(readonly userId: UserId) { super(); this.userId = userId; } } export class FakeActiveUserState extends FakeUserState implements ActiveUserState { [activeMarker]: true; changeActiveUser(userId: UserId) { this.userId = userId; } } export class FakeDerivedState implements DerivedState { // eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup stateSubject = new ReplaySubject(1); forceValue(value: T): Promise { this.stateSubject.next(value); return Promise.resolve(value); } forceValueMock = this.forceValue as jest.MockedFunction; get state$() { return this.stateSubject.asObservable(); } }