import { Container, Store } from '@servicetitan/react-ioc'; import { ContainerBuilder } from '../container-builder'; import { QueryClientStore } from '../../../utils/query-client.store'; import { QueryApiStore } from '../../../utils/query-api.store'; import type { QueryApiOptions } from '../../../utils/query.api'; import { getTypeName } from '../../../utils/client-helpers'; // Test store classes class TestStore extends Store { initialized = false; initialize = jest.fn(async () => { await Promise.resolve(); this.initialized = true; }); } class TestClass { value = 'test'; } class GlobalClientQueryStore extends QueryApiStore { get queryOptions(): QueryApiOptions { return { queryKey: ['test', 'global-client'], queryFn: () => Promise.resolve('ok'), globalClient: 'app', }; } } describe('ContainerBuilder', () => { let containerBuilder: ContainerBuilder; beforeEach(() => { jest.clearAllMocks(); containerBuilder = new ContainerBuilder(); }); describe('constructor', () => { it('should create container builder with default container', () => { const builder = new ContainerBuilder(); expect(builder.container).toBeInstanceOf(Container); }); it('should use provided container', () => { const customContainer = new Container(); const builder = new ContainerBuilder(customContainer); expect(builder.container).toBe(customContainer); }); it('should create with custom query client config', () => { const customConfig = { defaultOptions: { queries: { staleTime: 1000 }, }, }; const builder = new ContainerBuilder(undefined, customConfig).build(); expect(builder).toBeInstanceOf(ContainerBuilder); // Get the QueryClientStore from the container and verify config const queryClientStore = builder.container.get(QueryClientStore); expect(queryClientStore).toBeDefined(); // Access the queryClient and check its default options const defaultOptions = queryClientStore.queryClient.getDefaultOptions(); expect(defaultOptions.queries?.staleTime).toBe(1000); }); }); describe('add', () => { it('should add new token with custom value and return builder for chaining', () => { const testInstance = new TestClass(); const result = containerBuilder.add(TestClass, { useValue: testInstance }).build(); expect(result).toBe(containerBuilder); // Verify the instance is actually in the container const retrievedInstance = containerBuilder.container.get(TestClass); expect(retrievedInstance).toBe(testInstance); }); it('should add and retrieve custom value from container', () => { const testInstance = new TestClass(); testInstance.value = 'custom-test-value'; containerBuilder.add(TestClass, { useValue: testInstance }).build(); const retrievedInstance = containerBuilder.container.get(TestClass); expect(retrievedInstance).toBe(testInstance); expect(retrievedInstance.value).toBe('custom-test-value'); }); it('should add new token with class', () => { const result = containerBuilder.add(TestClass, { useClass: TestClass }).build(); expect(result).toBe(containerBuilder); }); it('should add and retrieve class instance from container', () => { containerBuilder.add(TestClass, { useClass: TestClass }).build(); const retrievedInstance = containerBuilder.container.get(TestClass); expect(retrievedInstance).toBeInstanceOf(TestClass); expect(retrievedInstance.value).toBe('test'); // default value }); it('should add token without custom value', () => { const result = containerBuilder.add(TestClass).build(); expect(result).toBe(containerBuilder); // Verify the class is in the container const retrievedInstance = containerBuilder.container.get(TestClass); expect(retrievedInstance).toBeInstanceOf(TestClass); expect(retrievedInstance.value).toBe('test'); // default value }); it('should add and retrieve store prototype from container', () => { containerBuilder.add(TestStore).build(); const retrievedStore = containerBuilder.container.get(TestStore); expect(retrievedStore).toBeInstanceOf(TestStore); expect(retrievedStore.initialized).toBe(false); expect(typeof retrievedStore.initialize).toBe('function'); }); it('should add symbol token with value and retrieve from container', () => { const TestSymbol = Symbol('TestSymbol'); const testValue = { symbolTest: 'success' }; containerBuilder.add(TestSymbol, { useValue: testValue }).build(); const retrievedValue = containerBuilder.container.get(TestSymbol) as typeof testValue; expect(retrievedValue).toBe(testValue); expect(retrievedValue.symbolTest).toBe('success'); }); it('should not overwrite existing token', () => { const firstValue = new TestClass(); const secondValue = new TestClass(); containerBuilder.add(TestClass, { useValue: firstValue }); containerBuilder.add(TestClass, { useValue: secondValue }).build(); // Should still work without throwing expect(containerBuilder).toBeInstanceOf(ContainerBuilder); // Verify the first value is still in the container (not overwritten) const retrievedValue = containerBuilder.container.get(TestClass); expect(retrievedValue).toBe(firstValue); }); it('should add symbol tokens', () => { const symbolToken = Symbol('TestService'); const testInstance = new TestClass(); const result = containerBuilder.add(symbolToken, { useValue: testInstance }).build(); expect(result).toBe(containerBuilder); // Verify the symbol token is in the container const retrievedInstance = containerBuilder.container.get(symbolToken); expect(retrievedInstance).toBe(testInstance); }); it('should add symbol tokens with class', () => { const symbolToken = Symbol('TestClass'); const result = containerBuilder.add(symbolToken, { useClass: TestClass }).build(); expect(result).toBe(containerBuilder); // Verify the symbol token with class is in the container const retrievedInstance = containerBuilder.container.get(symbolToken) as TestClass; expect(retrievedInstance).toBeInstanceOf(TestClass); expect(retrievedInstance.value).toBe('test'); // default value }); it('should handle multiple different symbol tokens', () => { const symbolToken1 = Symbol('Service1'); const symbolToken2 = Symbol('Service2'); const testInstance1 = new TestClass(); const testInstance2 = new TestClass(); const result = containerBuilder .add(symbolToken1, { useValue: testInstance1 }) .add(symbolToken2, { useValue: testInstance2 }) .build(); expect(result).toBe(containerBuilder); // Verify both symbol tokens are in the container const retrievedInstance1 = containerBuilder.container.get(symbolToken1); const retrievedInstance2 = containerBuilder.container.get(symbolToken2); expect(retrievedInstance1).toBe(testInstance1); expect(retrievedInstance2).toBe(testInstance2); }); }); describe('initialize', () => { it('should complete without errors when no additional stores added', async () => { // Only has the default QueryClientStore from constructor const { initialize } = containerBuilder.build(); await expect(initialize()).resolves.toBeUndefined(); }); it('should handle stores with ignore list', async () => { const { initialize } = containerBuilder.add(TestStore).build(); await expect(initialize([TestStore])).resolves.toBeUndefined(); }); it('should handle non-store tokens gracefully', async () => { // Create a fresh builder without the default QueryClientStore const { initialize } = new ContainerBuilder() .add(TestClass, { useValue: new TestClass() }) .build(); await expect(initialize()).resolves.toBeUndefined(); }); }); describe('integration', () => { it('should create full working container with multiple services and retrievals', () => { const testInstance = new TestClass(); testInstance.value = 'integration-test'; const symbolToken = Symbol('IntegrationService'); const symbolValue = { integration: 'complete' }; containerBuilder .add(TestClass, { useValue: testInstance }) .add(TestStore, { useClass: TestStore }) .add(symbolToken, { useValue: symbolValue }) .addQueryClient() .build(); // Verify all services can be retrieved const retrievedClass = containerBuilder.container.get(TestClass); const retrievedStore = containerBuilder.container.get(TestStore); const retrievedSymbol = containerBuilder.container.get( symbolToken ) as typeof symbolValue; const retrievedQueryClientStore = containerBuilder.container.get(QueryClientStore); expect(retrievedClass).toBe(testInstance); expect(retrievedClass.value).toBe('integration-test'); expect(retrievedStore).toBeInstanceOf(TestStore); expect(retrievedSymbol).toBe(symbolValue); expect(retrievedSymbol.integration).toBe('complete'); expect(retrievedQueryClientStore).toBeInstanceOf(QueryClientStore); }); }); describe('global client isolation', () => { afterEach(() => { window.queryClients?.clear(); }); it('should use injected test client for globalClient queries without window.queryClients', async () => { const { container, initialize } = new ContainerBuilder() .add(GlobalClientQueryStore) .build(); await initialize(); const store = container.get(GlobalClientQueryStore); const injected = container.get(QueryClientStore); expect(injected.isolateFromWindowClients).toBe(true); expect(store.query.clientStore).toBe(injected); expect(window.queryClients?.has(getTypeName('app'))).toBeFalsy(); }); }); });