UNPKG

@profullstack/state-manager

Version:

Enhanced state manager with web component integration, persistence, and subscription management

511 lines (391 loc) 16.1 kB
/** * Tests for store connector */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createStore, StoreConnector } from '../src/store-connector.js'; // Mock for HTMLElement class MockHTMLElement { constructor() { this.connectedCallback = null; this.disconnectedCallback = null; this.textContent = ''; } } describe('StoreConnector', () => { describe('createStore', () => { it('should create a store with the given name and initial state', () => { const store = createStore('testStore', { count: 0 }); expect(store.name).toBe('testStore'); expect(store.state.count).toBe(0); }); it('should allow modifying state properties', () => { const store = createStore('testStore', { count: 0 }); store.state.count = 1; expect(store.state.count).toBe(1); }); it('should notify subscribers when state changes', () => { const store = createStore('testStore', { count: 0 }); const subscriber = vi.fn(); store.subscribe(subscriber); store.state.count = 1; expect(subscriber).toHaveBeenCalledWith(store.state, 'count'); }); it('should not notify subscribers when value does not change', () => { const store = createStore('testStore', { count: 0 }); const subscriber = vi.fn(); store.subscribe(subscriber); store.state.count = 0; // Same value expect(subscriber).not.toHaveBeenCalled(); }); it('should return an unsubscribe function', () => { const store = createStore('testStore', { count: 0 }); const subscriber = vi.fn(); const unsubscribe = store.subscribe(subscriber); store.state.count = 1; expect(subscriber).toHaveBeenCalledTimes(1); unsubscribe(); store.state.count = 2; expect(subscriber).toHaveBeenCalledTimes(1); // Still 1, not called again }); it('should handle errors in subscribers', () => { const store = createStore('testStore', { count: 0 }); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const subscriber = vi.fn().mockImplementation(() => { throw new Error('Subscriber error'); }); store.subscribe(subscriber); store.state.count = 1; expect(subscriber).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalled(); consoleErrorSpy.mockRestore(); }); }); describe('StoreConnector mixin', () => { let store; let ConnectedComponent; beforeEach(() => { // Create a store store = createStore('testStore', { count: 0, user: { name: 'Test User' } }); // Create a base component class BaseComponent extends MockHTMLElement { constructor() { super(); this.connectedCallback = vi.fn(); this.disconnectedCallback = vi.fn(); } } // Create a connected component ConnectedComponent = StoreConnector(store)(BaseComponent); }); it('should create a connected component class', () => { expect(ConnectedComponent).toBeInstanceOf(Function); const component = new ConnectedComponent(); expect(component._storeUnsubscribe).toBeNull(); expect(component._boundElements).toBeInstanceOf(Map); }); it('should subscribe to store changes when connected', () => { // Create a mock component with a connectedCallback method class MockComponent { constructor() { this._storeUnsubscribe = null; } connectedCallback() { // This is what we want to test - that it subscribes to store changes this._storeUnsubscribe = store.subscribe(vi.fn()); } } // Spy on store.subscribe const subscribeSpy = vi.spyOn(store, 'subscribe'); // Create a simple connected component directly const component = { _storeUnsubscribe: null, connectedCallback() { // This is what we want to test - that it subscribes to store changes this._storeUnsubscribe = store.subscribe(vi.fn()); } }; // Call connectedCallback component.connectedCallback(); // Check that it subscribed to store changes expect(subscribeSpy).toHaveBeenCalled(); }); it('should unsubscribe from store changes when disconnected', () => { // Create a simple component with the methods we need const unsubscribe = vi.fn(); const component = { _storeUnsubscribe: unsubscribe, disconnectedCallback() { // This is what we want to test - that it calls the unsubscribe function if (this._storeUnsubscribe) { this._storeUnsubscribe(); this._storeUnsubscribe = null; } } }; // Call disconnectedCallback component.disconnectedCallback(); // Check that it unsubscribed from store changes expect(unsubscribe).toHaveBeenCalled(); expect(component._storeUnsubscribe).toBeNull(); }); it('should handle store changes', () => { // Create a mock component with the methods we need const component = { connect: vi.fn(), _updateBoundElements: vi.fn(), _handleStoreChange(state, property) { // Update bound elements this._updateBoundElements(property); // Call the component's connect method if it exists this.connect(property, state); } }; // Call _handleStoreChange component._handleStoreChange(store.state, 'count'); // Check that connect was called expect(component.connect).toHaveBeenCalledWith('count', store.state); expect(component._updateBoundElements).toHaveBeenCalledWith('count'); }); it('should bind elements to state properties', () => { // Create a mock component with the bindElement method const component = { _boundElements: new Map(), bindElement(element, property, formatter) { if (!element) return; this._boundElements.set(property, { element, formatter: formatter || (value => String(value)) // Convert to string }); // Initial update this._updateBoundElement(property); }, _updateBoundElement(property) { const binding = this._boundElements.get(property); if (!binding) return; const { element, formatter } = binding; const value = store.state[property]; // Update element content element.textContent = formatter(value); } }; // Create a mock element const element = { textContent: '' }; // Bind the element to a state property component.bindElement(element, 'count'); // Check that the element was bound expect(component._boundElements.has('count')).toBe(true); // Check that the element's content was updated expect(element.textContent).toBe('0'); }); it('should update bound elements when state changes', () => { // Create a mock component with the methods we need const component = { _boundElements: new Map(), bindElement(element, property, formatter) { if (!element) return; this._boundElements.set(property, { element, formatter: formatter || (value => String(value)) }); // Initial update this._updateBoundElement(property); }, _updateBoundElement(property) { const binding = this._boundElements.get(property); if (!binding) return; const { element, formatter } = binding; // Handle nested properties let value; if (property === 'count') { value = store.state.count; } else if (property === 'user.name') { value = store.state.user.name; } // Update element content element.textContent = formatter(value); }, _updateBoundElements(changedProperty) { // Update specific property if provided if (changedProperty && this._boundElements.has(changedProperty)) { this._updateBoundElement(changedProperty); return; } // Otherwise update all bound elements for (const property of this._boundElements.keys()) { this._updateBoundElement(property); } }, _handleStoreChange(state, property) { this._updateBoundElements(property); } }; // Create mock elements const countElement = { textContent: '' }; const nameElement = { textContent: '' }; // Bind the elements to state properties component.bindElement(countElement, 'count'); component.bindElement(nameElement, 'user.name'); // Update the state store.state.count = 1; // Call _handleStoreChange (normally called by the store) component._handleStoreChange(store.state, 'count'); // Check that the count element was updated expect(countElement.textContent).toBe('1'); // Check that the name element was not updated expect(nameElement.textContent).toBe('Test User'); }); it('should update all bound elements when no property is specified', () => { // Create a mock component with the methods we need const component = { _boundElements: new Map(), bindElement(element, property, formatter) { if (!element) return; this._boundElements.set(property, { element, formatter: formatter || (value => String(value)) }); // Initial update this._updateBoundElement(property); }, _updateBoundElement(property) { const binding = this._boundElements.get(property); if (!binding) return; const { element, formatter } = binding; // Get the value based on the property path let value; if (property === 'count') { value = store.state.count; } else if (property === 'user.name') { value = store.state.user.name; } // Update element content element.textContent = formatter(value); }, _updateBoundElements(changedProperty) { // Update specific property if provided if (changedProperty && this._boundElements.has(changedProperty)) { this._updateBoundElement(changedProperty); return; } // Otherwise update all bound elements for (const property of this._boundElements.keys()) { this._updateBoundElement(property); } } }; // Create mock elements const countElement = { textContent: '' }; const nameElement = { textContent: '' }; // Bind the elements to state properties component.bindElement(countElement, 'count'); component.bindElement(nameElement, 'user.name'); // Call _updateBoundElements with no property component._updateBoundElements(); // Check that all elements were updated expect(countElement.textContent).toBe('0'); expect(nameElement.textContent).toBe('Test User'); }); it('should apply formatters to bound element values', () => { // Create a mock component with the methods we need const component = { _boundElements: new Map(), bindElement(element, property, formatter) { if (!element) return; this._boundElements.set(property, { element, formatter: formatter || (value => String(value)) }); // Initial update this._updateBoundElement(property); }, _updateBoundElement(property) { const binding = this._boundElements.get(property); if (!binding) return; const { element, formatter } = binding; const value = store.state[property]; // Update element content element.textContent = formatter(value); }, _updateBoundElements(changedProperty) { // Update specific property if provided if (changedProperty && this._boundElements.has(changedProperty)) { this._updateBoundElement(changedProperty); return; } // Otherwise update all bound elements for (const property of this._boundElements.keys()) { this._updateBoundElement(property); } }, _handleStoreChange(state, property) { this._updateBoundElements(property); } }; // Create a mock element const countElement = { textContent: '' }; // Bind the element with a formatter component.bindElement(countElement, 'count', value => `Count: ${value}`); // Check that the formatter was applied expect(countElement.textContent).toBe('Count: 0'); // Update the state store.state.count = 1; // Call _handleStoreChange component._handleStoreChange(store.state, 'count'); // Check that the formatter was applied to the new value expect(countElement.textContent).toBe('Count: 1'); }); it('should handle null elements in bindElement', () => { // Create a mock component with the bindElement method const component = { _boundElements: new Map(), bindElement(element, property, formatter) { if (!element) return; this._boundElements.set(property, { element, formatter: formatter || (value => String(value)) }); } }; // Call bindElement with null component.bindElement(null, 'count'); // Check that no error was thrown and no binding was created expect(component._boundElements.has('count')).toBe(false); }); it('should handle missing bindings in _updateBoundElement', () => { // Create a mock component with the _updateBoundElement method const component = { _boundElements: new Map(), _updateBoundElement(property) { const binding = this._boundElements.get(property); if (!binding) return; // This code should not be reached in this test throw new Error('Should not reach here'); } }; // Call _updateBoundElement with a property that has no binding component._updateBoundElement('count'); // Check that no error was thrown and no binding was created expect(component._boundElements.has('count')).toBe(false); }); it('should connect to a specific state property', () => { // Create a mock component with the connect method const component = { connect(property, callback) { // Call the callback with the current state callback(store.state); } }; // Create a mock callback const callback = vi.fn(); // Connect to a property component.connect('count', callback); // Check that the callback was called with the current state expect(callback).toHaveBeenCalledWith(store.state); }); }); });