useLocalStorageState.spec.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {reactHooks, waitFor} from 'sentry-test/reactTestingLibrary';
  2. import localStorageWrapper from 'sentry/utils/localStorage';
  3. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  4. describe('useLocalStorageState', () => {
  5. beforeEach(() => {
  6. localStorageWrapper.clear();
  7. if (localStorageWrapper.length > 0) {
  8. throw new Error('localStorage was not cleared');
  9. }
  10. });
  11. it('throws if key is not a string', () => {
  12. const results = reactHooks.renderHook(() =>
  13. // @ts-expect-error force incorrect usage
  14. useLocalStorageState({}, 'default value')
  15. );
  16. expect(results.result.error).toBeInstanceOf(TypeError);
  17. expect(results.result.error?.message).toBe('useLocalStorage: key must be a string');
  18. });
  19. it('initialized with value', () => {
  20. const {result} = reactHooks.renderHook(() =>
  21. useLocalStorageState('key', 'default value')
  22. );
  23. expect(result.current[0]).toBe('default value');
  24. });
  25. it('initializes with init fn', () => {
  26. const initialize = jest.fn(() => 'default value');
  27. const {result} = reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  28. expect(initialize).toHaveBeenCalled();
  29. expect(result.current[0]).toBe('default value');
  30. });
  31. it('initializes with default value', () => {
  32. localStorageWrapper.setItem('key', JSON.stringify('initial storage value'));
  33. const {result} = reactHooks.renderHook(() =>
  34. useLocalStorageState('key', 'default value')
  35. );
  36. expect(result.current[0]).toBe('initial storage value');
  37. });
  38. it('sets new value', () => {
  39. const {result} = reactHooks.renderHook(() =>
  40. useLocalStorageState('key', 'default value')
  41. );
  42. reactHooks.act(() => {
  43. result.current[1]('new value');
  44. });
  45. expect(result.current[0]).toBe('new value');
  46. });
  47. it('updates localstorage value', async () => {
  48. const {result} = reactHooks.renderHook(() =>
  49. useLocalStorageState('key', 'default value')
  50. );
  51. const spy = jest.spyOn(Storage.prototype, 'setItem');
  52. reactHooks.act(() => {
  53. result.current[1]('new value');
  54. });
  55. // Exhaust task queue because setItem is scheduled as microtask
  56. await waitFor(() => {
  57. expect(spy).toHaveBeenCalledWith('key', JSON.stringify('new value'));
  58. });
  59. });
  60. it('when no value is present in storage, calls init with undefined and null', () => {
  61. const initialize = jest.fn(() => 'default value');
  62. reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  63. expect(initialize).toHaveBeenCalledWith(undefined, null);
  64. });
  65. it('when a value is present but cannot be parsed, calls init with undefined, null', () => {
  66. localStorageWrapper.setItem('key', JSON.stringify('invalid').slice(0, 5));
  67. const initialize = jest.fn(() => 'default value');
  68. reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  69. expect(initialize).toHaveBeenCalledWith(
  70. undefined,
  71. JSON.stringify('invalid json').slice(0, 5)
  72. );
  73. });
  74. it('when a value is present but cannot be parsed init can recover', () => {
  75. localStorageWrapper.setItem('key', JSON.stringify('invalid').slice(5, 9));
  76. const initialize = jest.fn((_decodedValue, encodedValue) => {
  77. const value = JSON.parse('"va' + encodedValue);
  78. return value;
  79. });
  80. const {result} = reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  81. expect(result.current[0]).toBe('valid');
  82. });
  83. it('when a value is present, init can transform it', () => {
  84. localStorageWrapper.setItem('key', JSON.stringify('valid json'));
  85. const initialize = jest.fn((decodedValue, _encodedValue) => {
  86. return 'super ' + decodedValue;
  87. });
  88. const {result} = reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  89. expect(result.current[0]).toBe('super valid json');
  90. });
  91. it('when a value is present and can be parsed, calls init with decoded and encoded value', () => {
  92. localStorageWrapper.setItem('key', JSON.stringify('valid json'));
  93. const initialize = jest.fn(() => 'default value');
  94. reactHooks.renderHook(() => useLocalStorageState('key', initialize));
  95. expect(initialize).toHaveBeenCalledWith('valid json', JSON.stringify('valid json'));
  96. });
  97. it('crashes with TypeError for unsupported primitives when they are recursive', () => {
  98. const recursiveReferenceMap = new Map();
  99. recursiveReferenceMap.set('key', recursiveReferenceMap);
  100. jest.spyOn(window, 'queueMicrotask').mockImplementation(cb => cb());
  101. const {result} = reactHooks.renderHook(() =>
  102. useLocalStorageState('key', recursiveReferenceMap)
  103. );
  104. try {
  105. result.current[1](recursiveReferenceMap);
  106. } catch (e) {
  107. expect(
  108. e.message.startsWith(
  109. `useLocalStorage: Native serialization of Map is not supported`
  110. )
  111. ).toBe(true);
  112. }
  113. });
  114. it('crashes with native error on recursive serialization of plain objects', () => {
  115. const recursiveObject: Record<string, any> = {};
  116. recursiveObject.key = recursiveObject;
  117. jest.spyOn(window, 'queueMicrotask').mockImplementation(cb => cb());
  118. const {result} = reactHooks.renderHook(() =>
  119. useLocalStorageState('key', recursiveObject)
  120. );
  121. try {
  122. result.current[1](recursiveObject);
  123. } catch (e) {
  124. expect(e.message.startsWith('Converting circular structure to JSON')).toBe(true);
  125. }
  126. });
  127. it.each([
  128. ['BigInt', BigInt(1)],
  129. ['RegExp (literal)', /regex/],
  130. ['RegExp (constructor)', new RegExp('regex')],
  131. ['Map', new Map()],
  132. ['Set', new Set()],
  133. ['WeakSet', new WeakSet()],
  134. ['WeakMap', new WeakMap()],
  135. // When invalid values are nested
  136. ['Map', {nested: new Map()}],
  137. ['Set', {nested: new Set()}],
  138. ['WeakSet', {nested: new WeakSet()}],
  139. ['WeakMap', {nested: new WeakMap()}],
  140. ])('when attempting to serialize a %s', (type, value) => {
  141. const results = reactHooks.renderHook(() => useLocalStorageState('key', value));
  142. // Immediately execute microtask so that the error is not thrown from the current execution stack and can be caught by a try/catch
  143. jest.spyOn(window, 'queueMicrotask').mockImplementation(cb => cb());
  144. try {
  145. results.result.current[1](value);
  146. } catch (e) {
  147. expect(
  148. e.message.startsWith(
  149. `useLocalStorage: Native serialization of ${
  150. type.split(' ')[0]
  151. } is not supported`
  152. )
  153. ).toBe(true);
  154. }
  155. });
  156. });