190 lines
6.1 KiB
TypeScript
190 lines
6.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import { RecoverableState } from '../../packages/core/src/recoverable_state';
|
|
import { MemoryStorageEmulator } from '../shared/MemoryStorageEmulator';
|
|
|
|
interface TestState {
|
|
counter: number;
|
|
message: string;
|
|
data: { nested: string };
|
|
}
|
|
|
|
describe('RecoverableState', () => {
|
|
const localStorageEmulator = new MemoryStorageEmulator();
|
|
|
|
beforeEach(async () => {
|
|
await localStorageEmulator.init();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await localStorageEmulator.destroy();
|
|
});
|
|
|
|
const defaultState: TestState = {
|
|
counter: 0,
|
|
message: 'hello',
|
|
data: { nested: 'value' },
|
|
};
|
|
|
|
test('should initialize with default state when persistence is disabled', async () => {
|
|
const recoverableState = new RecoverableState({
|
|
defaultState,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
const state = await recoverableState.initialize();
|
|
|
|
expect(state).toEqual(defaultState);
|
|
expect(state).not.toBe(defaultState); // Should be a deep copy
|
|
expect(recoverableState.currentValue).toEqual(defaultState);
|
|
});
|
|
|
|
test('should throw error when accessing currentValue before initialization', () => {
|
|
const recoverableState = new RecoverableState({
|
|
defaultState,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
expect(() => recoverableState.currentValue).toThrow('Recoverable state has not yet been loaded');
|
|
});
|
|
|
|
test('should allow state modification after initialization', async () => {
|
|
const recoverableState = new RecoverableState({
|
|
defaultState,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
await recoverableState.initialize();
|
|
|
|
// Modify the state
|
|
recoverableState.currentValue.counter = 42;
|
|
recoverableState.currentValue.message = 'modified';
|
|
recoverableState.currentValue.data.nested = 'new value';
|
|
|
|
expect(recoverableState.currentValue.counter).toBe(42);
|
|
expect(recoverableState.currentValue.message).toBe('modified');
|
|
expect(recoverableState.currentValue.data.nested).toBe('new value');
|
|
});
|
|
|
|
test('should reset state to default values', async () => {
|
|
const recoverableState = new RecoverableState({
|
|
defaultState,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
await recoverableState.initialize();
|
|
|
|
// Modify the state
|
|
recoverableState.currentValue.counter = 42;
|
|
recoverableState.currentValue.message = 'modified';
|
|
|
|
// Reset
|
|
await recoverableState.reset();
|
|
|
|
expect(recoverableState.currentValue).toEqual(defaultState);
|
|
expect(recoverableState.currentValue).not.toBe(defaultState); // Should be a new copy
|
|
});
|
|
|
|
test('should handle teardown gracefully when persistence is disabled', async () => {
|
|
const recoverableState = new RecoverableState({
|
|
defaultState,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
await recoverableState.initialize();
|
|
await expect(recoverableState.teardown()).resolves.not.toThrow();
|
|
});
|
|
|
|
test('should handle arrays and complex objects in deep copy', async () => {
|
|
const stateWithArray = {
|
|
items: [1, 2, { nested: 'value' }],
|
|
};
|
|
|
|
const recoverableState = new RecoverableState({
|
|
defaultState: stateWithArray,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: false,
|
|
});
|
|
|
|
await recoverableState.initialize();
|
|
|
|
// Modify array and nested object
|
|
recoverableState.currentValue.items.push(4);
|
|
(recoverableState.currentValue.items[2] as any).nested = 'modified';
|
|
|
|
// Reset should restore original values
|
|
await recoverableState.reset();
|
|
|
|
expect(recoverableState.currentValue.items).toEqual([1, 2, { nested: 'value' }]);
|
|
});
|
|
|
|
test('should handle custom classes with serialize/deserialize', async () => {
|
|
class CustomData {
|
|
constructor(
|
|
public value: string,
|
|
public count: number,
|
|
) {}
|
|
}
|
|
|
|
interface StateWithCustomClass {
|
|
data: CustomData;
|
|
name: string;
|
|
}
|
|
|
|
const stateWithCustomClass: StateWithCustomClass = {
|
|
data: new CustomData('test', 42),
|
|
name: 'example',
|
|
};
|
|
|
|
const serialize = vi.fn((state: StateWithCustomClass) =>
|
|
JSON.stringify({
|
|
data: {
|
|
value: state.data.value,
|
|
count: state.data.count,
|
|
},
|
|
name: state.name,
|
|
}),
|
|
);
|
|
|
|
const deserialize = vi.fn((serialized: string): StateWithCustomClass => {
|
|
const parsed = JSON.parse(serialized);
|
|
return {
|
|
data: new CustomData(parsed.data.value, parsed.data.count),
|
|
name: parsed.name,
|
|
};
|
|
});
|
|
|
|
const recoverableState = new RecoverableState({
|
|
defaultState: stateWithCustomClass,
|
|
persistStateKey: 'test-key',
|
|
persistenceEnabled: true,
|
|
serialize,
|
|
deserialize,
|
|
});
|
|
|
|
await recoverableState.initialize();
|
|
|
|
// Verify the custom class is properly handled
|
|
expect(recoverableState.currentValue.data).toBeInstanceOf(CustomData);
|
|
expect(recoverableState.currentValue.data.value).toBe('test');
|
|
expect(recoverableState.currentValue.name).toBe('example');
|
|
|
|
expect(deserialize).toHaveBeenCalled();
|
|
|
|
recoverableState.currentValue.data.value = 'updated';
|
|
await recoverableState.persistState();
|
|
|
|
const persistedState = JSON.parse((await localStorageEmulator.getKeyValueStore().getRecord('test-key'))?.value);
|
|
expect(persistedState).toMatchObject({
|
|
data: { value: 'updated' },
|
|
});
|
|
|
|
expect(serialize).toHaveBeenCalled();
|
|
});
|
|
});
|