@towns-protocol/sdk
Version:
For more details, visit the following resources:
321 lines • 13.4 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Observable } from '../../../observable/observable';
describe('Observable', () => {
describe('constructor and basic functionality', () => {
it('should initialize with a value', () => {
const obs = new Observable(5);
expect(obs.value).toBe(5);
});
it('should handle different types of initial values', () => {
const stringObs = new Observable('hello');
const objectObs = new Observable({ key: 'value' });
const arrayObs = new Observable([1, 2, 3]);
expect(stringObs.value).toBe('hello');
expect(objectObs.value).toEqual({ key: 'value' });
expect(arrayObs.value).toEqual([1, 2, 3]);
});
});
describe('setValue', () => {
it('should update the value', () => {
const obs = new Observable(5);
obs.setValue(10);
expect(obs.value).toBe(10);
});
it('should not trigger notifications when value is the same', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.setValue(5); // Same value
expect(subscriber).not.toHaveBeenCalled();
});
it('should trigger notifications when value changes', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.setValue(10);
expect(subscriber).toHaveBeenCalledWith(10, 5);
});
});
describe('set', () => {
it('should update value using a function', () => {
const obs = new Observable(5);
obs.set((prev) => prev * 2);
expect(obs.value).toBe(10);
});
it('should trigger notifications when using set', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.set((prev) => prev + 1);
expect(subscriber).toHaveBeenCalledWith(6, 5);
});
});
describe('subscribe', () => {
it('should call subscriber when value changes', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.setValue(10);
expect(subscriber).toHaveBeenCalledWith(10, 5);
});
it('should call multiple subscribers', () => {
const obs = new Observable(5);
const subscriber1 = vi.fn();
const subscriber2 = vi.fn();
obs.subscribe(subscriber1);
obs.subscribe(subscriber2);
obs.setValue(10);
expect(subscriber1).toHaveBeenCalledWith(10, 5);
expect(subscriber2).toHaveBeenCalledWith(10, 5);
});
it('should fire immediately when fireImediately option is true', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber, { fireImediately: true });
expect(subscriber).toHaveBeenCalledWith(5, 5);
});
it('should only fire once when once option is true', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber, { once: true });
obs.setValue(10);
obs.setValue(15);
expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledWith(10, 5);
});
it('should only fire when condition is met', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber, { condition: (value) => value > 10 });
obs.setValue(8); // Should not fire
obs.setValue(12); // Should fire
obs.setValue(15); // Should fire
expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber).toHaveBeenNthCalledWith(1, 12, 8);
expect(subscriber).toHaveBeenNthCalledWith(2, 15, 12);
});
it('should combine once and condition options', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
obs.subscribe(subscriber, {
once: true,
condition: (value) => value > 10,
});
obs.setValue(8); // Should not fire
obs.setValue(12); // Should fire once
obs.setValue(15); // Should not fire (already fired once)
expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenCalledWith(12, 8);
});
it('should return an unsubscribe function', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
const unsubscribe = obs.subscribe(subscriber);
obs.setValue(10);
expect(subscriber).toHaveBeenCalledTimes(1);
unsubscribe();
obs.setValue(15);
expect(subscriber).toHaveBeenCalledTimes(1); // Should not be called again
});
});
describe('unsubscribe', () => {
it('should remove a specific subscriber', () => {
const obs = new Observable(5);
const subscriber1 = vi.fn();
const subscriber2 = vi.fn();
obs.subscribe(subscriber1);
obs.subscribe(subscriber2);
obs.unsubscribe(subscriber1);
obs.setValue(10);
expect(subscriber1).not.toHaveBeenCalled();
expect(subscriber2).toHaveBeenCalledWith(10, 5);
});
it('should handle unsubscribing non-existent subscriber', () => {
const obs = new Observable(5);
const subscriber = vi.fn();
// Should not throw error
expect(() => obs.unsubscribe(subscriber)).not.toThrow();
});
});
describe('when', () => {
it('should resolve when condition is immediately met', async () => {
const obs = new Observable(15);
const result = await obs.when((value) => value > 10);
expect(result).toBe(15);
});
it('should resolve when condition becomes true', async () => {
const obs = new Observable(5);
const promise = obs.when((value) => value > 10);
setTimeout(() => obs.setValue(15), 10);
const result = await promise;
expect(result).toBe(15);
});
it('should timeout when condition is not met', async () => {
const obs = new Observable(5);
await expect(obs.when((value) => value > 10, { timeoutMs: 50 })).rejects.toThrow('Timeout waiting for condition');
});
it('should include description in timeout error', async () => {
const obs = new Observable(5);
await expect(obs.when((value) => value > 10, {
timeoutMs: 50,
description: 'value to exceed 10',
})).rejects.toThrow('Timeout waiting for condition value to exceed 10');
});
});
describe('map', () => {
it('should create a mapped observable', () => {
const obs = new Observable(5);
const mapped = obs.map((value) => ({ doubled: value * 2 }));
expect(mapped.value).toEqual({ doubled: 10 });
});
it('should update mapped observable when source changes', () => {
const obs = new Observable(5);
const mapped = obs.map((value) => ({ doubled: value * 2 }));
obs.setValue(10);
expect(mapped.value).toEqual({ doubled: 20 });
});
it('should pass previous value and state to map function', () => {
const obs = new Observable(5);
const mapFn = vi.fn((value, prevValue, state) => ({
current: value,
previous: prevValue,
count: (state?.count || 0) + 1,
}));
const mapped = obs.map(mapFn);
expect(mapped.value).toEqual({
current: 5,
previous: 5,
count: 1,
});
// Initial call
expect(mapFn).toHaveBeenCalledWith(5, 5);
obs.setValue(10);
// Should be called with current state
expect(mapFn).toHaveBeenLastCalledWith(10, 5, { current: 5, previous: 5, count: 1 });
expect(mapped.value).toEqual({
current: 10,
previous: 5,
count: 2,
});
});
it('should dispose mapped observable correctly', () => {
const obs = new Observable(5);
const mapped = obs.map((value) => ({ doubled: value * 2 }));
mapped.dispose();
obs.setValue(10);
// Mapped observable should not update after disposal
expect(mapped.value).toEqual({ doubled: 10 });
});
});
describe('throttle', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should create a throttled observable with initial value', () => {
const obs = new Observable(5);
const throttled = obs.throttle(100);
expect(throttled.value).toBe(5);
});
it('should throttle rapid value changes', () => {
const obs = new Observable(5);
const throttled = obs.throttle(100);
const subscriber = vi.fn();
throttled.subscribe(subscriber);
obs.setValue(10);
obs.setValue(15);
obs.setValue(20);
// Should not have updated yet
expect(subscriber).not.toHaveBeenCalled();
expect(throttled.value).toBe(5);
// Fast forward time
vi.advanceTimersByTime(100);
// Should update with the last value
expect(subscriber).toHaveBeenCalledWith(20, 5);
expect(throttled.value).toBe(20);
});
it('should handle multiple throttle periods', () => {
const obs = new Observable(5);
const throttled = obs.throttle(100);
obs.setValue(10);
vi.advanceTimersByTime(100);
expect(throttled.value).toBe(10);
obs.setValue(20);
obs.setValue(30);
vi.advanceTimersByTime(100);
expect(throttled.value).toBe(30);
});
it('should dispose throttled observable correctly', () => {
const obs = new Observable(5);
const throttled = obs.throttle(100);
obs.setValue(10);
throttled.dispose();
// Should clear timeout and not update
vi.advanceTimersByTime(100);
expect(throttled.value).toBe(5);
});
});
describe('dispose', () => {
it('should clear all subscribers', () => {
const obs = new Observable(5);
const subscriber1 = vi.fn();
const subscriber2 = vi.fn();
obs.subscribe(subscriber1);
obs.subscribe(subscriber2);
obs.dispose();
obs.setValue(10);
expect(subscriber1).not.toHaveBeenCalled();
expect(subscriber2).not.toHaveBeenCalled();
});
it('should call dispose function if set', () => {
const obs = new Observable(5);
const disposeFn = vi.fn();
obs._dispose = disposeFn;
obs.dispose();
expect(disposeFn).toHaveBeenCalled();
});
});
describe('edge cases and error handling', () => {
it('should handle undefined values', () => {
const obs = new Observable(undefined);
expect(obs.value).toBeUndefined();
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.setValue(5);
expect(subscriber).toHaveBeenCalledWith(5, undefined);
});
it('should handle null values', () => {
const obs = new Observable(null);
expect(obs.value).toBeNull();
obs.setValue(5);
expect(obs.value).toBe(5);
});
it('should handle complex object comparisons', () => {
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obs = new Observable(obj1);
const subscriber = vi.fn();
obs.subscribe(subscriber);
obs.setValue(obj2); // Different object reference but same content
expect(subscriber).toHaveBeenCalledWith(obj2, obj1);
});
it('should handle subscription during notification', () => {
const obs = new Observable(5);
const subscriber1 = vi.fn();
const subscriber2 = vi.fn();
obs.subscribe((value) => {
subscriber1(value);
// Subscribe during notification
obs.subscribe(subscriber2);
});
obs.setValue(10);
obs.setValue(15);
expect(subscriber1).toHaveBeenCalledTimes(2);
expect(subscriber2).toHaveBeenCalledTimes(1); // Only called for second setValue
});
});
});
//# sourceMappingURL=observable.test.js.map