use-persisted-state.test.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { useEffect } from 'react'
  4. import { render, screen } from '@testing-library/react'
  5. import usePersistedState from '../../../../frontend/js/shared/hooks/use-persisted-state'
  6. import localStorage from '@/infrastructure/local-storage'
  7. describe('usePersistedState', function () {
  8. beforeEach(function () {
  9. sinon.spy(window.Storage.prototype, 'getItem')
  10. sinon.spy(window.Storage.prototype, 'removeItem')
  11. sinon.spy(window.Storage.prototype, 'setItem')
  12. })
  13. afterEach(function () {
  14. sinon.restore()
  15. })
  16. it('reads the value from localStorage', function () {
  17. const key = 'test'
  18. localStorage.setItem(key, 'foo')
  19. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  20. const Test = () => {
  21. const [value] = usePersistedState<string>(key)
  22. return <div>{value}</div>
  23. }
  24. render(<Test />)
  25. screen.getByText('foo')
  26. expect(window.Storage.prototype.getItem).to.have.callCount(1)
  27. expect(window.Storage.prototype.removeItem).to.have.callCount(0)
  28. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  29. expect(localStorage.getItem(key)).to.equal('foo')
  30. })
  31. it('uses the default value without storing anything', function () {
  32. const key = 'test:default'
  33. const Test = () => {
  34. const [value] = usePersistedState(key, 'foo')
  35. return <div>{value}</div>
  36. }
  37. render(<Test />)
  38. screen.getByText('foo')
  39. expect(window.Storage.prototype.getItem).to.have.callCount(1)
  40. expect(window.Storage.prototype.removeItem).to.have.callCount(0)
  41. expect(window.Storage.prototype.setItem).to.have.callCount(0)
  42. expect(localStorage.getItem(key)).to.be.null
  43. })
  44. it('stores the new value in localStorage', function () {
  45. const key = 'test:store'
  46. localStorage.setItem(key, 'foo')
  47. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  48. const Test = () => {
  49. const [value, setValue] = usePersistedState(key, 'bar')
  50. useEffect(() => {
  51. setValue('baz')
  52. }, [setValue])
  53. return <div>{value}</div>
  54. }
  55. render(<Test />)
  56. screen.getByText('baz')
  57. expect(window.Storage.prototype.getItem).to.have.callCount(1)
  58. expect(window.Storage.prototype.removeItem).to.have.callCount(0)
  59. expect(window.Storage.prototype.setItem).to.have.callCount(2)
  60. expect(localStorage.getItem(key)).to.equal('baz')
  61. })
  62. it('removes the value from localStorage if it equals the default value', function () {
  63. const key = 'test:store-default'
  64. localStorage.setItem(key, 'foo')
  65. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  66. const Test = () => {
  67. const [value, setValue] = usePersistedState(key, 'bar')
  68. useEffect(() => {
  69. // set a different value
  70. setValue('baz')
  71. expect(localStorage.getItem(key)).to.equal('baz')
  72. // set the default value again
  73. setValue('bar')
  74. }, [setValue])
  75. return <div>{value}</div>
  76. }
  77. render(<Test />)
  78. screen.getByText('bar')
  79. expect(window.Storage.prototype.getItem).to.have.callCount(2)
  80. expect(window.Storage.prototype.removeItem).to.have.callCount(1)
  81. expect(window.Storage.prototype.setItem).to.have.callCount(2)
  82. expect(localStorage.getItem(key)).to.be.null
  83. })
  84. it('handles function values', function () {
  85. const key = 'test:store'
  86. localStorage.setItem(key, 'foo')
  87. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  88. const Test = () => {
  89. const [value, setValue] = usePersistedState<string>(key)
  90. useEffect(() => {
  91. setValue(value => value + 'bar')
  92. }, [setValue])
  93. return <div>{value}</div>
  94. }
  95. render(<Test />)
  96. screen.getByText('foobar')
  97. expect(window.Storage.prototype.getItem).to.have.callCount(1)
  98. expect(window.Storage.prototype.removeItem).to.have.callCount(0)
  99. expect(window.Storage.prototype.setItem).to.have.callCount(2)
  100. expect(localStorage.getItem(key)).to.equal('foobar')
  101. })
  102. it('converts persisted value (string to boolean)', function () {
  103. const key = 'test:convert'
  104. localStorage.setItem(key, 'yep')
  105. const Test = () => {
  106. const [value, setValue] = usePersistedState(key, true, {
  107. converter: {
  108. toPersisted(value) {
  109. return value ? 'yep' : 'nope'
  110. },
  111. fromPersisted(persistedValue) {
  112. return persistedValue === 'yep'
  113. },
  114. },
  115. })
  116. useEffect(() => {
  117. setValue(false)
  118. }, [setValue])
  119. return <div>{String(value)}</div>
  120. }
  121. render(<Test />)
  122. screen.getByText('false')
  123. expect(localStorage.getItem(key)).to.equal('nope')
  124. })
  125. it('handles syncing values via storage event', async function () {
  126. const key = 'test:sync'
  127. localStorage.setItem(key, 'foo')
  128. expect(window.Storage.prototype.setItem).to.have.callCount(1)
  129. // listen for storage events
  130. const storageEventListener = sinon.stub()
  131. window.addEventListener('storage', storageEventListener)
  132. const Test = () => {
  133. const [value, setValue] = usePersistedState(key, 'bar', { listen: true })
  134. useEffect(() => {
  135. setValue('baz')
  136. }, [setValue])
  137. return <div>{value}</div>
  138. }
  139. render(<Test />)
  140. screen.getByText('baz')
  141. expect(window.Storage.prototype.getItem).to.have.callCount(1)
  142. expect(window.Storage.prototype.removeItem).to.have.callCount(0)
  143. expect(window.Storage.prototype.setItem).to.have.callCount(2)
  144. expect(localStorage.getItem(key)).to.equal('baz')
  145. expect(storageEventListener).to.have.callCount(0)
  146. // set the new value in localStorage
  147. localStorage.setItem(key, 'cat')
  148. // dispatch a "storage" event and check that it's picked up by the hook
  149. window.dispatchEvent(new StorageEvent('storage', { key }))
  150. await screen.findByText('cat')
  151. expect(storageEventListener).to.have.callCount(1)
  152. })
  153. })