philomena/assets/js/utils/__tests__/store.spec.ts
2024-07-03 22:54:14 +02:00

222 lines
7.5 KiB
TypeScript

import store, { lastUpdatedSuffix } from '../store';
import { mockStorageImpl } from '../../../test/mock-storage';
import { getRandomIntBetween } from '../../../test/randomness';
import { fireEvent } from '@testing-library/dom';
import { mockDateNow } from '../../../test/mock-date-now';
describe('Store utilities', () => {
const { setItemSpy, getItemSpy, removeItemSpy, forceStorageError, setStorageValue } = mockStorageImpl();
const initialDateNow = 1640645432942;
describe('set', () => {
it('should be able to set various types of items correctly', () => {
const mockKey = `mock-set-key-${getRandomIntBetween(1, 10)}`;
const mockValues = [
1,
false,
null,
Math.random(),
'some string\n value\twith trailing whitespace ',
{ complex: { value: true, key: 'string' } },
];
mockValues.forEach((mockValue, i) => {
const result = store.set(mockKey, mockValue);
expect(setItemSpy).toHaveBeenNthCalledWith(i + 1, mockKey, JSON.stringify(mockValue));
expect(result).toBe(true);
});
expect(setItemSpy).toHaveBeenCalledTimes(mockValues.length);
});
it('should gracefully handle failure to set key', () => {
const mockKey = 'mock-set-key';
const mockValue = Math.random();
let result: boolean | undefined;
forceStorageError(() => {
result = store.set(mockKey, mockValue);
});
expect(result).toBe(false);
});
});
describe('get', () => {
it('should be able to get various types of items correctly', () => {
const initialValues = {
int: 1,
boolean: false,
null: null,
float: Math.random(),
string: '\t\t\thello\nthere\n ',
object: {
rolling: {
in: {
the: {
deep: true,
},
},
},
},
};
const initialValueKeys = Object.keys(initialValues) as (keyof typeof initialValues)[];
setStorageValue(
initialValueKeys.reduce((acc, key) => {
return { ...acc, [key]: JSON.stringify(initialValues[key]) };
}, {}),
);
initialValueKeys.forEach((key, i) => {
const result = store.get(key);
expect(getItemSpy).toHaveBeenNthCalledWith(i + 1, key);
expect(result).toEqual(initialValues[key]);
});
expect(getItemSpy).toHaveBeenCalledTimes(initialValueKeys.length);
});
it('should return original value if item cannot be parsed', () => {
const mockKey = 'mock-get-key';
const malformedValue = '({[+:"`';
setStorageValue({
[mockKey]: malformedValue,
});
const result = store.get(mockKey);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(getItemSpy).toHaveBeenNthCalledWith(1, mockKey);
expect(result).toBe(malformedValue);
});
it('should return null if item is not set', () => {
const mockKey = `mock-get-key-${getRandomIntBetween(1, 10)}`;
const result = store.get(mockKey);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(getItemSpy).toHaveBeenNthCalledWith(1, mockKey);
expect(result).toBe(null);
});
});
describe('remove', () => {
it('should remove the provided key', () => {
const mockKey = `mock-remove-key-${getRandomIntBetween(1, 10)}`;
const result = store.remove(mockKey);
expect(removeItemSpy).toHaveBeenCalledTimes(1);
expect(removeItemSpy).toHaveBeenNthCalledWith(1, mockKey);
expect(result).toBe(true);
});
it('should gracefully handle failure to remove key', () => {
const mockKey = `mock-remove-key-${getRandomIntBetween(1, 10)}`;
let result: boolean | undefined;
forceStorageError(() => {
result = store.remove(mockKey);
});
expect(result).toBe(false);
});
});
describe('watch', () => {
it('should attach a storage event listener and fire when the provide key changes', () => {
const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`;
const mockValue = Math.random();
const mockCallback = vi.fn();
setStorageValue({
[mockKey]: JSON.stringify(mockValue),
});
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const cleanup = store.watch(mockKey, mockCallback);
// Should not get the item just yet, only register the event handler
expect(getItemSpy).not.toHaveBeenCalled();
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
expect(addEventListenerSpy.mock.calls[0][0]).toEqual('storage');
// Should not call callback for unknown key
let storageEvent = new StorageEvent('storage', { key: 'unknown-key' });
fireEvent(window, storageEvent);
expect(getItemSpy).not.toHaveBeenCalled();
expect(mockCallback).not.toHaveBeenCalled();
// Should call callback with the value from the store
storageEvent = new StorageEvent('storage', { key: mockKey });
fireEvent(window, storageEvent);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenNthCalledWith(1, mockValue);
// Remove the listener
cleanup();
storageEvent = new StorageEvent('storage', { key: mockKey });
fireEvent(window, storageEvent);
// Expect unchanged call counts due to removed handler
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenNthCalledWith(1, mockValue);
});
});
describe('setWithExpireTime', () => {
mockDateNow(initialDateNow);
it('should set both original and last update key', () => {
const mockKey = `mock-setWithExpireTime-key-${getRandomIntBetween(1, 10)}`;
const mockValue = 'mock value';
const mockMaxAge = 3600;
store.setWithExpireTime(mockKey, mockValue, mockMaxAge);
expect(setItemSpy).toHaveBeenCalledTimes(2);
expect(setItemSpy).toHaveBeenNthCalledWith(1, mockKey, JSON.stringify(mockValue));
expect(setItemSpy).toHaveBeenNthCalledWith(
2,
mockKey + lastUpdatedSuffix,
JSON.stringify(initialDateNow + mockMaxAge),
);
});
});
describe('hasExpired', () => {
mockDateNow(initialDateNow);
const mockKey = `mock-hasExpired-key-${getRandomIntBetween(1, 10)}`;
const mockLastUpdatedKey = mockKey + lastUpdatedSuffix;
it('should return true for values that have no expiration key', () => {
const result = store.hasExpired('undefined-key');
expect(result).toBe(true);
});
it('should return true for keys with last update timestamp smaller than the current time', () => {
setStorageValue({
[mockLastUpdatedKey]: JSON.stringify(initialDateNow - 1),
});
const result = store.hasExpired(mockKey);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(result).toBe(true);
});
it('should return false for keys with last update timestamp equal to the current time', () => {
setStorageValue({
[mockLastUpdatedKey]: JSON.stringify(initialDateNow),
});
const result = store.hasExpired(mockKey);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(result).toBe(false);
});
it('should return false for keys with last update timestamp greater than the current time', () => {
setStorageValue({
[mockLastUpdatedKey]: JSON.stringify(initialDateNow + 1),
});
const result = store.hasExpired(mockKey);
expect(getItemSpy).toHaveBeenCalledTimes(1);
expect(result).toBe(false);
});
});
});