@kadconsulting/dry
Version:
KAD Reusable Component Library
300 lines • 16.1 kB
JavaScript
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useLocalStorageForKey, FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE, } from './useLocalStorageForKey';
import { useState, useMemo } from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor, act } from '@testing-library/react';
import { LocalStorageValueTypes } from './types';
describe('useLocalStorageForKey', () => {
beforeEach(() => {
jest.spyOn(window.localStorage, 'setItem');
jest.spyOn(window.localStorage, 'getItem');
});
afterEach(() => jest.clearAllMocks());
it('performs feature detection on mount - success', () => {
// ARRANGE
const { testKey, supported, unsupported } = SHARED_TEST_VALUES();
mockSuccessfulFeatureDetection();
const TestComponent = () => {
const { isSupported } = useLocalStorageForKey(testKey, {
valueType: LocalStorageValueTypes.STRING,
debug: true,
});
return _jsx(_Fragment, { children: isSupported ? supported : unsupported });
};
// ACT
render(_jsx(TestComponent, {}));
// ASSERT
assertSuccessfulFeatureDetection();
expect(screen.getByText(supported)).toBeInTheDocument();
});
it('performs feature detection on mount - failure', () => {
// ARRANGE
const { testKey, supported, unsupported } = SHARED_TEST_VALUES();
const { originalGetValue, originalSetValue } = mockFailedFeatureDetection();
const TestComponent = () => {
const { isSupported } = useLocalStorageForKey(testKey, {
valueType: LocalStorageValueTypes.STRING,
});
return _jsx(_Fragment, { children: isSupported ? supported : unsupported });
};
// ACT
render(_jsx(TestComponent, {}));
// ASSERT - feature detection starts by setting a value, which will fail, so we'd expect it to be called once, and getItem to never be called thereafter
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE);
expect(Storage.prototype.getItem).toHaveBeenCalledTimes(0);
expect(screen.getByText(unsupported)).toBeInTheDocument();
// CLEANUP
Storage.prototype.getItem = originalGetValue;
Storage.prototype.setItem = originalSetValue;
});
it('logs feature-detection errors if debug mode is enabled', () => {
// ARRANGE
const { testKey, supported, unsupported } = SHARED_TEST_VALUES();
/** Suppress console.error output in std out for tests, but ensure it gets called */
const actualConsoleDotError = console.error;
console.error = jest.fn();
const { originalGetValue } = mockFailedFeatureDetection();
const TestComponent = () => {
const { isSupported } = useLocalStorageForKey(testKey, {
debug: true,
valueType: LocalStorageValueTypes.STRING,
});
return _jsx(_Fragment, { children: isSupported ? supported : unsupported });
};
// ACT
render(_jsx(TestComponent, {}));
expect(console.error).toHaveBeenCalledTimes(1);
// CLEANUP
console.error = actualConsoleDotError;
Storage.prototype.getItem = originalGetValue;
});
it('sets / gets string values in localStorage', async () => {
// ARRANGE
const { testId, testKey, testValue, getButtonLabel, setButtonLabel } = SHARED_TEST_VALUES();
mockSuccessfulFeatureDetection();
// ACT
render(_jsx(TestComponent, { testId: testId, testKey: testKey, testValue: testValue, setButtonLabel: setButtonLabel, getButtonLabel: getButtonLabel, valueType: LocalStorageValueTypes.STRING }));
// ASSERT
// Not necessary to test this here, but it's included for maintainability, so that it's clear why the mocks need to be cleared
assertSuccessfulFeatureDetection();
// Assert test circumstances don't create a false-positive
expect(screen.queryByText(testValue)).not.toBeInTheDocument();
// Prepare for user event (set button)
Storage.prototype.getItem.mockClear();
Storage.prototype.setItem.mockClear();
// ACT
await act(async () => await userEvent.click(screen.getByText(setButtonLabel)));
// ASSERT
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(testKey, testValue);
// Prepare for user event (get button)
Storage.prototype.getItem.mockClear();
Storage.prototype.getItem.mockReturnValueOnce(testValue);
// ACT
await act(async () => await userEvent.click(screen.getByText(getButtonLabel)));
// ASSERT
expect(Storage.prototype.getItem).toHaveBeenCalledWith(testKey);
expect(Storage.prototype.getItem).toHaveReturnedWith(testValue);
await waitFor(() => expect(screen.getByTestId(testId)).toContainHTML(testValue));
});
it('sets / gets number values in localStorage', async () => {
// ARRANGE
const { testId, testKey, testValue, getButtonLabel, setButtonLabel } = SHARED_TEST_VALUES({ value: 1 });
mockSuccessfulFeatureDetection();
// ACT
render(_jsx(TestComponent, { testId: testId, testKey: testKey, testValue: testValue, setButtonLabel: setButtonLabel, getButtonLabel: getButtonLabel, valueType: LocalStorageValueTypes.NUMBER }));
assertSuccessfulFeatureDetection();
// Assert test circumstances don't create a false-positive
expect(screen.getByTestId(testId)).not.toContainHTML(`${testValue}`);
// Prepare for user event (set button)
Storage.prototype.getItem.mockClear();
Storage.prototype.setItem.mockClear();
// ACT
await act(async () => await userEvent.click(screen.getByText(setButtonLabel)));
// ASSERT
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(testKey, testValue);
// Prepare for user event (get button)
Storage.prototype.getItem.mockClear();
Storage.prototype.getItem.mockImplementationOnce(() => testValue);
// ACT
await act(async () => await userEvent.click(screen.getByText(getButtonLabel)));
// ASSERT
expect(Storage.prototype.getItem).toHaveBeenCalledWith(testKey);
expect(Storage.prototype.getItem).toHaveReturnedWith(testValue);
await waitFor(() => expect(screen.getByTestId(testId)).toContainHTML(`${testValue}`));
});
it('sets / gets boolean values in localStorage', async () => {
// ARRANGE
const { testId, testKey, testValue, getButtonLabel, setButtonLabel } = SHARED_TEST_VALUES({
value: true,
});
mockSuccessfulFeatureDetection();
// ACT
render(_jsx(TestComponent, { testId: testId, testKey: testKey, testValue: testValue, setButtonLabel: setButtonLabel, getButtonLabel: getButtonLabel, valueType: LocalStorageValueTypes.BOOLEAN }));
// Feature detection (this doesn't have to be included here, but it's nice for maintainability to understand why mocks need to be cleared)
expect(Storage.prototype.getItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.getItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE);
// Assert test circumstances don't create a false-positive
expect(screen.getByTestId(testId)).not.toContainHTML(JSON.stringify(testValue));
// Prepare for user event (set button)
Storage.prototype.getItem.mockClear();
Storage.prototype.setItem.mockClear();
// ACT
await act(async () => await userEvent.click(screen.getByText(setButtonLabel)));
// ASSERT
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
// Prepare for user event (get button)
Storage.prototype.getItem.mockClear();
Storage.prototype.getItem.mockImplementationOnce(() => JSON.stringify(testValue));
// ACT
await act(async () => await userEvent.click(screen.getByText(getButtonLabel)));
// ASSERT
expect(Storage.prototype.getItem).toHaveBeenCalledWith(testKey);
expect(Storage.prototype.getItem).toHaveReturnedWith(JSON.stringify(testValue));
await waitFor(() => expect(screen.getByTestId(testId)).toContainHTML(JSON.stringify(testValue)));
});
it('sets / gets object values in localStorage', async () => {
// ARRANGE
const { testId, testKey, testValue, getButtonLabel, setButtonLabel } = SHARED_TEST_VALUES({
value: {
id: 1,
name: 'test',
},
});
mockSuccessfulFeatureDetection();
// ACT
render(_jsx(TestComponent, { testId: testId, testKey: testKey, testValue: testValue, setButtonLabel: setButtonLabel, getButtonLabel: getButtonLabel, valueType: LocalStorageValueTypes.OBJECT }));
assertSuccessfulFeatureDetection();
// Assert test circumstances don't create a false-positive
expect(screen.getByTestId(testId)).not.toContainHTML(JSON.stringify(testValue));
// Prepare for user event (set button)
Storage.prototype.getItem.mockClear();
Storage.prototype.setItem.mockClear();
// ACT
await act(async () => await userEvent.click(screen.getByText(setButtonLabel)));
// ASSERT
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(testKey, testValue);
// Prepare for user event (get button)
Storage.prototype.getItem.mockClear();
Storage.prototype.getItem.mockImplementationOnce(() => JSON.stringify(testValue));
// ACT
await act(async () => await userEvent.click(screen.getByText(getButtonLabel)));
// ASSERT
expect(Storage.prototype.getItem).toHaveBeenCalledWith(testKey);
expect(Storage.prototype.getItem).toHaveReturnedWith(JSON.stringify(testValue));
await waitFor(() => expect(screen.getByTestId(testId)).toContainHTML(JSON.stringify(testValue)));
});
it('sets / gets array values in localStorage', async () => {
// ARRANGE
const { testId, testKey, testValue, getButtonLabel, setButtonLabel } = SHARED_TEST_VALUES({
value: [
{
id: 1,
name: 'test1',
},
{
id: 2,
name: 'test2',
},
],
});
mockSuccessfulFeatureDetection();
// ACT
render(_jsx(TestComponent, { testId: testId, testKey: testKey, testValue: testValue, setButtonLabel: setButtonLabel, getButtonLabel: getButtonLabel, valueType: LocalStorageValueTypes.OBJECT }));
// Feature detection (this doesn't have to be included here, but it's nice for maintainability to understand why mocks need to be cleared)
expect(Storage.prototype.getItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.getItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE);
// Assert test circumstances don't create a false-positive
expect(screen.getByTestId(testId)).not.toContainHTML(JSON.stringify(testValue));
// Prepare for user event (set button)
Storage.prototype.getItem.mockClear();
Storage.prototype.setItem.mockClear();
// ACT
await act(async () => await userEvent.click(screen.getByText(setButtonLabel)));
// ASSERT
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(testKey, testValue);
// Prepare for user event (get button)
Storage.prototype.getItem.mockClear();
Storage.prototype.getItem.mockImplementationOnce(() => JSON.stringify(testValue));
// ACT
await act(async () => await userEvent.click(screen.getByText(getButtonLabel)));
// ASSERT
expect(Storage.prototype.getItem).toHaveBeenCalledWith(testKey);
expect(Storage.prototype.getItem).toHaveReturnedWith(JSON.stringify(testValue));
await waitFor(() => expect(screen.getByTestId(testId)).toContainHTML(JSON.stringify(testValue)));
});
});
const SHARED_TEST_VALUES = ({ value, } = { value: 'testValue' }) => ({
testKey: 'testKey',
testValue: value,
testId: 'testId',
supported: 'supported',
unsupported: 'unsupported',
getButtonLabel: 'Get value',
setButtonLabel: 'Set value',
});
/**
* A component that allows userEvents to trigger storage events and
* renders the results for DOM
*/
const TestComponent = ({ testKey, testValue, testId, valueType, setButtonLabel, getButtonLabel, }) => {
const testStorage = useLocalStorageForKey(testKey, {
valueType,
debug: true,
});
const [value, setValue] = useState(null);
/** Support rendering of JSON strings for complex data types */
const processed = useMemo(() => {
const processedValues = {
[LocalStorageValueTypes.NUMBER]: `${value}`,
[LocalStorageValueTypes.BOOLEAN]: `${value}`,
[LocalStorageValueTypes.STRING]: value,
[LocalStorageValueTypes.ARRAY]: JSON.stringify(value),
[LocalStorageValueTypes.OBJECT]: JSON.stringify(value),
};
return processedValues[valueType];
}, [value, valueType]);
return (_jsxs(_Fragment, { children: [_jsx("button", { type: 'button', onClick: () => testStorage.set(testValue), children: setButtonLabel }), _jsx("button", { type: 'button', onClick: () => setValue(testStorage.get()), children: getButtonLabel }), _jsx("div", { "data-testid": testId, children: processed })] }));
};
const mockSuccessfulFeatureDetection = () => {
Storage.prototype.setItem = jest
.fn()
/** Mock the localStorage wrapper hook's feature detection */
.mockImplementationOnce(() => undefined);
Storage.prototype.getItem = jest
.fn()
/** Mock the localStorage wrapper hook's feature detection */
.mockImplementationOnce(() => FEATURE_DETECTION_TEST_VALUE);
};
const mockFailedFeatureDetection = () => {
const originalSetValue = Storage.prototype.setItem;
const originalGetValue = Storage.prototype.getItem;
Storage.prototype.setItem = jest
.fn((cb) => cb())
.mockImplementationOnce((cb) => cb(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE))
.mockRejectedValueOnce(new DOMException('QuotaExceededError'));
Storage.prototype.getItem = jest
.fn((cb) => cb())
.mockImplementationOnce((cb) => cb(FEATURE_DETECTION_TEST_KEY))
.mockRejectedValueOnce(new DOMException('SecurityError'));
/** Return the original value of the mocked methods so that they may be restored in cleanups */
return {
originalSetValue,
originalGetValue,
};
};
const assertSuccessfulFeatureDetection = () => {
expect(Storage.prototype.setItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.setItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY, FEATURE_DETECTION_TEST_VALUE);
expect(Storage.prototype.getItem).toHaveBeenCalledTimes(1);
expect(Storage.prototype.getItem).toHaveBeenCalledWith(FEATURE_DETECTION_TEST_KEY);
};
//# sourceMappingURL=useLocalStorageKey.test.js.map