import {beforeEach, describe, expect, it, vi} from 'vitest' import {type DocumentSource} from '../config/sanityConfig' import { bindActionByDataset, bindActionBySource, bindActionBySourceAndPerspective, bindActionGlobally, createActionBinder, } from './createActionBinder' import {createSanityInstance} from './createSanityInstance' import {createStoreInstance} from './createStoreInstance' // Mock store instance creation for testing vi.mock('./createStoreInstance', () => ({ createStoreInstance: vi.fn(() => ({state: {counter: 0}, dispose: vi.fn()})), })) beforeEach(() => vi.mocked(createStoreInstance).mockClear()) describe('createActionBinder', () => { it('should bind an action and call it with correct context and parameters, using caching', () => { const binder = createActionBinder((..._rest) => ({name: ''})) const storeDefinition = { name: 'TestStore', getInitialState: () => ({counter: 0}), } // Action that increments counter by given value const action = vi.fn((context, increment: number) => { context.state.counter += increment return context.state.counter }) const boundAction = binder(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) // First call creates store instance const result1 = boundAction(instance, 5) expect(result1).toBe(5) // Second call reuses cached store const result2 = boundAction(instance, 5) expect(result2).toBe(10) expect(action).toHaveBeenCalledTimes(2) expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1) }) it('should create separate store instances for different composite keys', () => { const binder = createActionBinder(({config: {projectId, dataset}}, ..._rest) => ({ name: `${projectId}.${dataset}`, })) const storeDefinition = { name: 'TestStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((context, val: number) => { context.state.counter += val return context.state.counter }) const boundAction = binder(storeDefinition, action) const instanceA = createSanityInstance({projectId: 'p1', dataset: 'd1'}) const instanceB = createSanityInstance({projectId: 'p2', dataset: 'd2'}) const resultA = boundAction(instanceA, 3) const resultB = boundAction(instanceB, 4) expect(resultA).toBe(3) expect(resultB).toBe(4) expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(2) }) it('should dispose the store instance when the last instance is disposed', () => { const binder = createActionBinder((..._rest) => ({name: ''})) const storeDefinition = { name: 'TestStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((context) => context.state.counter) const boundAction = binder(storeDefinition, action) const instance1 = createSanityInstance({projectId: 'p1', dataset: 'd1'}) const instance2 = createSanityInstance({projectId: 'p1', dataset: 'd1'}) // Call action on both instances boundAction(instance1) boundAction(instance2) expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1) const [{value: storeInstance}] = vi.mocked(createStoreInstance).mock.results expect(storeInstance).toBeDefined() // First disposal shouldn't trigger store disposal instance1.dispose() expect(storeInstance.dispose).not.toHaveBeenCalled() // Last disposal should trigger store disposal instance2.dispose() expect(storeInstance.dispose).toHaveBeenCalledTimes(1) }) }) describe('bindActionByDataset', () => { it('should work correctly when projectId and dataset are provided', () => { const storeDefinition = { name: 'DSStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context, {value}: {value: string}) => value) const boundAction = bindActionByDataset(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) const result = boundAction(instance, {value: 'hello'}) expect(result).toBe('hello') }) it('should throw an error if projectId or dataset is missing', () => { const storeDefinition = { name: 'DSStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context, _?) => 'fail') const boundAction = bindActionByDataset(storeDefinition, action) // Instance with missing dataset const instance = createSanityInstance({projectId: 'proj1', dataset: ''}) expect(() => boundAction(instance)).toThrow( 'This API requires a project ID and dataset configured.', ) }) }) describe('bindActionGlobally', () => { it('should work correctly ignoring config in key generation', () => { const storeDefinition = { name: 'GlobalStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context, x: number) => x) const boundAction = bindActionGlobally(storeDefinition, action) // Create instances with different configs const instance1 = createSanityInstance({projectId: 'any', dataset: 'any'}) const instance2 = createSanityInstance({projectId: 'different', dataset: 'config'}) // Both instances should use the same store const result1 = boundAction(instance1, 42) const result2 = boundAction(instance2, 99) expect(result1).toBe(42) expect(result2).toBe(99) // Verify single store instance used expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1) // Verify action called with correct arguments expect(action).toHaveBeenNthCalledWith(1, expect.anything(), 42) expect(action).toHaveBeenNthCalledWith(2, expect.anything(), 99) // Test disposal tracking const [{value: storeInstance}] = vi.mocked(createStoreInstance).mock.results instance1.dispose() expect(storeInstance.dispose).not.toHaveBeenCalled() instance2.dispose() expect(storeInstance.dispose).toHaveBeenCalledTimes(1) }) }) describe('bindActionBySource', () => { it('should throw an error when provided an invalid source', () => { const storeDefinition = { name: 'SourceStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySource(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) expect(() => boundAction(instance, {source: {invalid: 'source'} as unknown as DocumentSource}), ).toThrow('Received invalid source:') }) it('should throw an error when no source provided and projectId/dataset are missing', () => { const storeDefinition = { name: 'SourceStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySource(storeDefinition, action) const instance = createSanityInstance({projectId: '', dataset: ''}) expect(() => boundAction(instance, {})).toThrow( 'This API requires a project ID and dataset configured.', ) }) it('should work correctly with a valid dataset source', () => { const storeDefinition = { name: 'SourceStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySource(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) const result = boundAction(instance, { source: {projectId: 'proj2', dataset: 'ds2'}, }) expect(result).toBe('success') }) }) describe('bindActionBySourceAndPerspective', () => { it('should throw an error when provided an invalid source', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) expect(() => boundAction(instance, { source: {invalid: 'source'} as unknown as DocumentSource, perspective: 'drafts', }), ).toThrow('Received invalid source:') }) it('should throw an error when no source provided and projectId/dataset are missing', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({projectId: '', dataset: ''}) expect(() => boundAction(instance, {perspective: 'drafts'})).toThrow( 'This API requires a project ID and dataset configured.', ) }) it('should work correctly with a valid dataset source and explicit perspective', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) const result = boundAction(instance, { source: {projectId: 'proj2', dataset: 'ds2'}, perspective: 'drafts', }) expect(result).toBe('success') }) it('should work correctly with valid dataset source and no perspective (falls back to drafts)', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) const result = boundAction(instance, { source: {projectId: 'proj1', dataset: 'ds1'}, }) expect(result).toBe('success') }) it('should use instance.config.perspective when options.perspective is not provided', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((context) => context.key) const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({ projectId: 'proj1', dataset: 'ds1', perspective: 'published', }) const result = boundAction(instance, {}) expect(result).toEqual( expect.objectContaining({ name: 'proj1.ds1:published', perspective: 'published', }), ) }) it('should create separate store instances for different perspectives', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((context, _options, increment: number) => { context.state.counter += increment return context.state.counter }) const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) // Use unique project/dataset so we don't reuse stores from other tests const instance = createSanityInstance({ projectId: 'perspective-isolation', dataset: 'ds1', }) const resultDrafts = boundAction(instance, {perspective: 'drafts'}, 3) const resultPublished = boundAction(instance, {perspective: 'published'}, 4) expect(resultDrafts).toBe(3) expect(resultPublished).toBe(4) // Two stores: one for drafts, one for published expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(2) }) it('should create separate store instance for release perspective', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((_context) => 'success') const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'}) const result = boundAction(instance, { perspective: {releaseName: 'release1'}, }) expect(result).toBe('success') expect(vi.mocked(createStoreInstance)).toHaveBeenCalledWith( instance, expect.objectContaining({ name: 'proj1.ds1:release1', perspective: {releaseName: 'release1'}, }), storeDefinition, ) }) it('should reuse same store when same source and perspective are used', () => { const storeDefinition = { name: 'PerspectiveStore', getInitialState: () => ({counter: 0}), } const action = vi.fn((context, _options, increment: number) => { context.state.counter += increment return context.state.counter }) const boundAction = bindActionBySourceAndPerspective(storeDefinition, action) // Use unique project/dataset so we don't reuse stores from other tests const instance = createSanityInstance({ projectId: 'perspective-reuse', dataset: 'ds1', }) const result1 = boundAction(instance, {perspective: 'drafts'}, 2) const result2 = boundAction(instance, {perspective: 'drafts'}, 3) expect(result1).toBe(2) expect(result2).toBe(5) expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1) }) })