@idiosync/react-observable
Version:
State management control layer for React projects
279 lines (278 loc) • 17.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importDefault(require("react"));
const react_2 = require("@testing-library/react");
const use_effect_stream_1 = require("./use-effect-stream");
const observable_1 = require("../factories/observable");
const context_1 = require("../store/context");
// Test component that uses the hook
const TestComponent = ({ initialise, deps, }) => {
const value = (0, use_effect_stream_1.useEffectStream)(initialise, deps);
return react_1.default.createElement("div", { "data-testid": "value" }, JSON.stringify(value));
};
describe('useEffectStream', () => {
describe('Basic functionality', () => {
it('should return observable value', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'test value' });
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('"test value"');
});
it('should update when observable changes', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'initial' });
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('"initial"');
(0, react_2.act)(() => {
obs.set('updated');
});
expect(react_2.screen.getByTestId('value')).toHaveTextContent('"updated"');
});
it('should handle complex objects', () => {
const obs = (0, observable_1.createObservable)({
initialValue: { name: 'John', age: 25, active: true },
});
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('{"name":"John","age":25,"active":true}');
});
});
describe('Dependencies', () => {
it('should re-initialize when dependencies change', () => {
const mockInitialise = jest.fn(({ $, store }) => (0, observable_1.createObservable)({ initialValue: 'default' }));
const { rerender } = (0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise, deps: ['dep1'] })));
expect(mockInitialise).toHaveBeenCalledTimes(1);
rerender(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise, deps: ['dep2'] })));
expect(mockInitialise).toHaveBeenCalledTimes(2);
});
it('should not re-initialize when dependencies are the same', () => {
const mockInitialise = jest.fn(({ $, store }) => (0, observable_1.createObservable)({ initialValue: 'default' }));
const { rerender } = (0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise, deps: ['same'] })));
expect(mockInitialise).toHaveBeenCalledTimes(1);
rerender(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise, deps: ['same'] })));
expect(mockInitialise).toHaveBeenCalledTimes(1);
});
it('should handle undefined dependencies', () => {
const mockInitialise = jest.fn(({ $, store }) => (0, observable_1.createObservable)({ initialValue: 'default' }));
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise })));
expect(mockInitialise).toHaveBeenCalledTimes(1);
});
});
describe('Store integration', () => {
it('should provide store to initialise function', () => {
const mockInitialise = jest.fn();
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: mockInitialise })));
expect(mockInitialise).toHaveBeenCalledWith({
$: expect.any(Object),
store: expect.any(Object),
});
});
it('should allow accessing store observables', () => {
const TestStoreComponent = () => {
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => {
var _a;
return (((_a = store.test) === null || _a === void 0 ? void 0 : _a.value$) || (0, observable_1.createObservable)({ initialValue: 'default' }));
});
return react_1.default.createElement("div", { "data-testid": "value" }, value);
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestStoreComponent, null)));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('default');
});
});
describe('Stream operations', () => {
it('should handle stream operations', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 5 });
const TestStreamComponent = () => {
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => {
return obs.stream((val) => val * 2);
});
return react_1.default.createElement("div", { "data-testid": "value" }, value);
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestStreamComponent, null)));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('10');
});
it('should handle async stream operations', async () => {
const obs = (0, observable_1.createObservable)({ initialValue: 5 });
const TestAsyncStreamComponent = () => {
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => {
return obs.streamAsync(async (val) => {
await new Promise((resolve) => setTimeout(resolve, 10));
return val * 2;
});
});
return react_1.default.createElement("div", { "data-testid": "value" }, value);
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestAsyncStreamComponent, null)));
// Wait for async operation
await (0, react_2.act)(async () => {
await new Promise((resolve) => setTimeout(resolve, 20));
});
expect(react_2.screen.getByTestId('value')).toHaveTextContent('10');
});
it('should handle combineLatestFrom', () => {
const obs1 = (0, observable_1.createObservable)({ initialValue: 'a' });
const obs2 = (0, observable_1.createObservable)({ initialValue: 1 });
const TestCombineComponent = () => {
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => {
return obs1.combineLatestFrom(obs2);
});
return react_1.default.createElement("div", { "data-testid": "value" }, JSON.stringify(value));
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestCombineComponent, null)));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('["a",1]');
});
it('should handle withLatestFrom', () => {
const obs1 = (0, observable_1.createObservable)({ initialValue: 'a' });
const obs2 = (0, observable_1.createObservable)({ initialValue: 1 });
const TestWithLatestComponent = () => {
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => {
return obs1.withLatestFrom(obs2);
});
return react_1.default.createElement("div", { "data-testid": "value" }, JSON.stringify(value));
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestWithLatestComponent, null)));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('["a",1]');
});
});
describe('Subscription management', () => {
it('should subscribe to observable on mount', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'initial' });
const subscribeSpy = jest.spyOn(obs, 'subscribe');
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(subscribeSpy).toHaveBeenCalled();
});
it('should unsubscribe on unmount', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'initial' });
const unsubscribeSpy = jest.fn();
jest.spyOn(obs, 'subscribe').mockReturnValue(unsubscribeSpy);
const { unmount } = (0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
unmount();
expect(unsubscribeSpy).toHaveBeenCalled();
});
it('should handle multiple subscriptions', () => {
const obs1 = (0, observable_1.createObservable)({ initialValue: 'obs1' });
const obs2 = (0, observable_1.createObservable)({ initialValue: 'obs2' });
const MultiObsComponent = () => {
const value1 = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => obs1);
const value2 = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => obs2);
return (react_1.default.createElement("div", null,
react_1.default.createElement("div", { "data-testid": "value1" }, value1),
react_1.default.createElement("div", { "data-testid": "value2" }, value2)));
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(MultiObsComponent, null)));
expect(react_2.screen.getByTestId('value1')).toHaveTextContent('obs1');
expect(react_2.screen.getByTestId('value2')).toHaveTextContent('obs2');
});
});
describe('Error handling', () => {
it('should handle observable errors gracefully', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'initial' });
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
(0, react_2.act)(() => {
obs.emitError(new Error('Test error'));
});
// Should not crash the component
expect(react_2.screen.getByTestId('value')).toBeInTheDocument();
consoleSpy.mockRestore();
});
it('should handle initialise function errors', () => {
const errorInitialise = () => {
throw new Error('Initialise error');
};
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
expect(() => {
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: errorInitialise })));
}).toThrow('Initialise error');
consoleSpy.mockRestore();
});
});
describe('Performance and optimization', () => {
it('should not re-render when observable value is the same', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'same' });
const renderSpy = jest.fn();
const TestRenderComponent = () => {
renderSpy();
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => obs);
return react_1.default.createElement("div", { "data-testid": "value" }, value);
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestRenderComponent, null)));
const initialRenderCount = renderSpy.mock.calls.length;
(0, react_2.act)(() => {
obs.set('same'); // Same value
});
expect(renderSpy.mock.calls.length).toBe(initialRenderCount);
});
it('should re-render when observable value changes', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'initial' });
const renderSpy = jest.fn();
const TestRenderComponent = () => {
renderSpy();
const value = (0, use_effect_stream_1.useEffectStream)(({ $, store }) => obs);
return react_1.default.createElement("div", { "data-testid": "value" }, value);
};
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestRenderComponent, null)));
const initialRenderCount = renderSpy.mock.calls.length;
(0, react_2.act)(() => {
obs.set('updated'); // Different value
});
expect(renderSpy.mock.calls.length).toBeGreaterThan(initialRenderCount);
});
});
describe('Edge cases', () => {
it('should handle undefined observable values', () => {
const obs = (0, observable_1.createObservable)();
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('null');
});
it('should handle null observable values', () => {
const obs = (0, observable_1.createObservable)({ initialValue: null });
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('null');
});
it('should handle function initial values', () => {
const obs = (0, observable_1.createObservable)({ initialValue: () => 'function value' });
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('"function value"');
});
});
describe('Provider context', () => {
it('should work without provider (fallback behavior)', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'test' });
// This should not crash even without provider
expect(() => {
(0, react_2.render)(react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs }));
}).not.toThrow();
});
it('should use provider context when available', () => {
const obs = (0, observable_1.createObservable)({ initialValue: 'test' });
(0, react_2.render)(react_1.default.createElement(context_1.ReactObservableProvider, null,
react_1.default.createElement(TestComponent, { initialise: ({ $, store }) => obs })));
expect(react_2.screen.getByTestId('value')).toHaveTextContent('"test"');
});
});
});