Files
apify--crawlee/test/core/recoverable_state.test.ts
2026-07-13 13:23:39 +08:00

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();
});
});