import { describe, it, expect } from 'vitest'; import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState'; import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub'; import { ApplicationContextStub } from '@tests/unit/shared/Stubs/ApplicationContextStub'; import type { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ApplicationContextChangedEventStub } from '@tests/unit/shared/Stubs/ApplicationContextChangedEventStub'; import { OperatingSystem } from '@/domain/OperatingSystem'; import type { IApplicationContext } from '@/application/Context/IApplicationContext'; import type { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection'; import { EventSubscriptionCollectionStub } from '@tests/unit/shared/Stubs/EventSubscriptionCollectionStub'; describe('UseCollectionState', () => { describe('listens to contextChanged event', () => { it('registers new event listener', () => { // arrange const events = new EventSubscriptionCollectionStub(); const expectedSubscriptionsCount = 1; // act new UseCollectionStateBuilder() .withEvents(events) .build(); // assert const actualSubscriptionsCount = events.subscriptionCount; expect(actualSubscriptionsCount).to.equal(expectedSubscriptionsCount); }); it('does not modify the state after event listener is unsubscribed', () => { // arrange const events = new EventSubscriptionCollectionStub(); const oldState = new CategoryCollectionStateStub(); const newState = new CategoryCollectionStateStub(); const context = new ApplicationContextStub() .withState(oldState); // act const { currentState } = new UseCollectionStateBuilder() .withContext(context) .withEvents(events) .build(); const stateModifierEvent = events.mostRecentSubscription; stateModifierEvent?.unsubscribe(); context.dispatchContextChange( new ApplicationContextChangedEventStub().withNewState(newState), ); // assert expect(stateModifierEvent).toBeDefined(); expect(currentState.value).to.equal(oldState); }); }); describe('currentContext', () => { it('returns current context', () => { // arrange const expectedContext = new ApplicationContextStub(); // act const { currentContext } = new UseCollectionStateBuilder() .withContext(expectedContext) .build(); // assert expect(currentContext).to.equal(expectedContext); }); }); describe('currentState', () => { it('returns current collection state', () => { // arrange const expected = new CategoryCollectionStateStub(); const context = new ApplicationContextStub() .withState(expected); // act const { currentState } = new UseCollectionStateBuilder() .withContext(context) .build(); // assert expect(currentState.value).to.equal(expected); }); it('returns changed collection state', () => { // arrange const expectedNewState = new CategoryCollectionStateStub(); const context = new ApplicationContextStub(); const { currentState } = new UseCollectionStateBuilder() .withContext(context) .build(); // act context.dispatchContextChange( new ApplicationContextChangedEventStub().withNewState(expectedNewState), ); // assert expect(currentState.value).to.equal(expectedNewState); }); }); describe('onStateChange', () => { it('call handler when context state changes', () => { // arrange const expected = true; const context = new ApplicationContextStub(); const { onStateChange } = new UseCollectionStateBuilder() .withContext(context) .build(); let wasCalled = false; // act onStateChange(() => { wasCalled = true; }); context.dispatchContextChange(); // assert expect(wasCalled).to.equal(expected); }); it('call handler immediately when immediate is true', () => { // arrange const expected = true; const { onStateChange } = new UseCollectionStateBuilder().build(); let wasCalled = false; // act onStateChange(() => { wasCalled = true; }, { immediate: true }); // assert expect(wasCalled).to.equal(expected); }); it('does not call handler immediately when immediate is false', () => { // arrange const expected = false; const { onStateChange } = new UseCollectionStateBuilder().build(); let wasCalled = false; // act onStateChange(() => { wasCalled = true; }, { immediate: false }); // assert expect(wasCalled).to.equal(expected); }); it('call multiple handlers when context state changes', () => { // arrange const expected = 5; const context = new ApplicationContextStub(); const { onStateChange } = new UseCollectionStateBuilder() .withContext(context) .build(); let totalCalled = 0; // act onStateChange(() => { totalCalled++; }, { immediate: false }); for (let i = 0; i < expected; i++) { context.dispatchContextChange(); } // assert expect(totalCalled).to.equal(expected); }); it('call handler with new state after state changes', () => { // arrange const expected = new CategoryCollectionStateStub(); let actual: IReadOnlyCategoryCollectionState | undefined; const context = new ApplicationContextStub(); const { onStateChange } = new UseCollectionStateBuilder() .withContext(context) .build(); // act onStateChange((newState) => { actual = newState; }); context.dispatchContextChange( new ApplicationContextChangedEventStub().withNewState(expected), ); // assert expect(actual).to.equal(expected); }); it('call handler with old state after state changes', () => { // arrange const expectedState = new CategoryCollectionStateStub(); let actualState: IReadOnlyCategoryCollectionState | undefined; const context = new ApplicationContextStub(); const { onStateChange } = new UseCollectionStateBuilder() .withContext(context) .build(); // act onStateChange((_, oldState) => { actualState = oldState; }); context.dispatchContextChange( new ApplicationContextChangedEventStub().withOldState(expectedState), ); // assert expect(actualState).to.equal(expectedState); }); describe('listens to contextChanged event', () => { it('registers new event listener', () => { // arrange const events = new EventSubscriptionCollectionStub(); const { onStateChange } = new UseCollectionStateBuilder() .withEvents(events) .build(); const expectedSubscriptionsCount = 1; // act events.unsubscribeAll(); // clean count for event subscriptions before onStateChange(() => { /* NO OP */ }); // assert const actualSubscriptionsCount = events.subscriptionCount; expect(actualSubscriptionsCount).to.equal(expectedSubscriptionsCount); }); it('onStateChange is not called once event is unsubscribed', () => { // arrange let isCallbackCalled = false; const callback = () => { isCallbackCalled = true; }; const context = new ApplicationContextStub(); const events = new EventSubscriptionCollectionStub(); const { onStateChange } = new UseCollectionStateBuilder() .withEvents(events) .withContext(context) .build(); // act onStateChange(callback); const stateChangeEvent = events.mostRecentSubscription; stateChangeEvent?.unsubscribe(); context.dispatchContextChange(); // assert expect(stateChangeEvent).toBeDefined(); expect(isCallbackCalled).to.equal(false); }); }); }); describe('modifyCurrentState', () => { it('modifies current collection state', () => { // arrange const oldOs = OperatingSystem.Windows; const newOs = OperatingSystem.Linux; const state = new CategoryCollectionStateStub() .withOs(oldOs); const context = new ApplicationContextStub() .withState(state); const { modifyCurrentState } = new UseCollectionStateBuilder() .withContext(context) .build(); // act modifyCurrentState((mutableState) => { const stubState = (mutableState as CategoryCollectionStateStub); stubState.withOs(newOs); }); const actualOs = context.state.collection.os; // assert expect(actualOs).to.equal(newOs); }); }); describe('modifyCurrentContext', () => { it('modifies the current context', () => { // arrange const oldState = new CategoryCollectionStateStub() .withOs(OperatingSystem.Linux); const newState = new CategoryCollectionStateStub() .withOs(OperatingSystem.macOS); const context = new ApplicationContextStub() .withState(oldState); const { modifyCurrentContext } = new UseCollectionStateBuilder() .withContext(context) .build(); // act modifyCurrentContext((mutableContext) => { const contextStub = mutableContext as ApplicationContextStub; contextStub.withState(newState); }); const actualState = context.state; // assert expect(actualState).to.equal(newState); }); }); }); class UseCollectionStateBuilder { private context: IApplicationContext = new ApplicationContextStub(); private events: IEventSubscriptionCollection = new EventSubscriptionCollectionStub(); public withContext(context: IApplicationContext): this { this.context = context; return this; } public withEvents(events: IEventSubscriptionCollection): this { this.events = events; return this; } public build(): ReturnType { return useCollectionState(this.context, this.events); } }