UNPKG

@kadconsulting/dry

Version:
300 lines 16.1 kB
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