/** * need to update test environment so structuredClone works appropriately * @jest-environment ../shared/test.environment.ts */ import { Observable, of } from "rxjs"; import { awaitAsync, trackEmissions } from "@bitwarden/core-test-utils"; import { DeriveDefinition, KeyDefinition, StateDefinition, UserKeyDefinition, } from "@bitwarden/state"; import { FakeActiveUserAccessor, FakeActiveUserStateProvider, FakeDerivedStateProvider, FakeGlobalStateProvider, FakeSingleUserStateProvider, } from "@bitwarden/state-test-utils"; import { UserId } from "@bitwarden/user-core"; import { DefaultStateProvider } from "./default-state.provider"; describe("DefaultStateProvider", () => { let sut: DefaultStateProvider; let activeUserStateProvider: FakeActiveUserStateProvider; let singleUserStateProvider: FakeSingleUserStateProvider; let globalStateProvider: FakeGlobalStateProvider; let derivedStateProvider: FakeDerivedStateProvider; let activeAccountAccessor: FakeActiveUserAccessor; const userId = "fakeUserId" as UserId; beforeEach(() => { activeAccountAccessor = new FakeActiveUserAccessor(userId); activeUserStateProvider = new FakeActiveUserStateProvider(activeAccountAccessor); singleUserStateProvider = new FakeSingleUserStateProvider(); globalStateProvider = new FakeGlobalStateProvider(); derivedStateProvider = new FakeDerivedStateProvider(); sut = new DefaultStateProvider( activeUserStateProvider, singleUserStateProvider, globalStateProvider, derivedStateProvider, ); }); describe("activeUserId$", () => { it("should track the active User id from active user state provider", () => { expect(sut.activeUserId$).toBe(activeUserStateProvider.activeUserId$); }); }); describe.each([ [ "getUserState$", (keyDefinition: UserKeyDefinition, userId?: UserId) => sut.getUserState$(keyDefinition, userId), ], [ "getUserStateOrDefault$", (keyDefinition: UserKeyDefinition, userId?: UserId) => sut.getUserStateOrDefault$(keyDefinition, { userId: userId }), ], ])( "Shared behavior for %s", ( _testName: string, methodUnderTest: ( keyDefinition: UserKeyDefinition, userId?: UserId, ) => Observable, ) => { const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, clearOn: [], }, ); it("should follow the specified user if userId is provided", async () => { const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); const emissions = trackEmissions(methodUnderTest(keyDefinition, userId)); state.nextState("value2"); state.nextState("value3"); expect(emissions).toEqual(["value", "value2", "value3"]); }); it("should follow the current active user if no userId is provided", async () => { activeAccountAccessor.switch(userId); const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); const emissions = trackEmissions(methodUnderTest(keyDefinition)); state.nextState("value2"); state.nextState("value3"); expect(emissions).toEqual(["value", "value2", "value3"]); }); it("should continue to follow the state of the user that was active when called, even if active user changes", async () => { const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); const emissions = trackEmissions(methodUnderTest(keyDefinition)); activeAccountAccessor.switch("newUserId" as UserId); const newUserEmissions = trackEmissions(sut.getUserState$(keyDefinition)); state.nextState("value2"); state.nextState("value3"); expect(emissions).toEqual(["value", "value2", "value3"]); expect(newUserEmissions).toEqual([null]); }); }, ); describe("getUserState$", () => { const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, clearOn: [], }, ); it("should not emit any values until a truthy user id is supplied", async () => { activeAccountAccessor.switch(null); const state = singleUserStateProvider.getFake(userId, keyDefinition); state.nextState("value"); const emissions = trackEmissions(sut.getUserState$(keyDefinition)); await awaitAsync(); expect(emissions).toHaveLength(0); activeAccountAccessor.switch(userId); await awaitAsync(); expect(emissions).toEqual(["value"]); }); }); describe("getUserStateOrDefault$", () => { const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, clearOn: [], }, ); it("should emit default value if no userId supplied and first active user id emission in falsy", async () => { activeAccountAccessor.switch(null); const emissions = trackEmissions( sut.getUserStateOrDefault$(keyDefinition, { userId: undefined, defaultValue: "I'm default!", }), ); expect(emissions).toEqual(["I'm default!"]); }); }); describe("setUserState", () => { const keyDefinition = new UserKeyDefinition( new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, clearOn: [], }, ); it("should set the state for the active user if no userId is provided", async () => { const value = "value"; await sut.setUserState(keyDefinition, value); const state = activeUserStateProvider.getFake(keyDefinition); expect(state.nextMock).toHaveBeenCalledWith([expect.any(String), value]); }); it("should not set state for a single user if no userId is provided", async () => { const value = "value"; await sut.setUserState(keyDefinition, value); const state = singleUserStateProvider.getFake(userId, keyDefinition); expect(state.nextMock).not.toHaveBeenCalled(); }); it("should set the state for the provided userId", async () => { const value = "value"; await sut.setUserState(keyDefinition, value, userId); const state = singleUserStateProvider.getFake(userId, keyDefinition); expect(state.nextMock).toHaveBeenCalledWith(value); }); it("should not set the active user state if userId is provided", async () => { const value = "value"; await sut.setUserState(keyDefinition, value, userId); const state = activeUserStateProvider.getFake(keyDefinition); expect(state.nextMock).not.toHaveBeenCalled(); }); }); it("should bind the activeUserStateProvider", () => { const keyDefinition = new UserKeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: () => null, clearOn: [], }); const existing = activeUserStateProvider.get(keyDefinition); const actual = sut.getActive(keyDefinition); expect(actual).toBe(existing); }); it("should bind the singleUserStateProvider", () => { const userId = "user" as UserId; const keyDefinition = new UserKeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: () => null, clearOn: [], }); const existing = singleUserStateProvider.get(userId, keyDefinition); const actual = sut.getUser(userId, keyDefinition); expect(actual).toBe(existing); }); it("should bind the globalStateProvider", () => { const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: () => null, }); const existing = globalStateProvider.get(keyDefinition); const actual = sut.getGlobal(keyDefinition); expect(actual).toBe(existing); }); it("should bind the derivedStateProvider", () => { const derivedDefinition = new DeriveDefinition(new StateDefinition("test", "disk"), "test", { derive: () => null, deserializer: () => null, }); const parentState$ = of(null); const existing = derivedStateProvider.get(parentState$, derivedDefinition, {}); const actual = sut.getDerived(parentState$, derivedDefinition, {}); expect(actual).toBe(existing); }); });