react-scope-value-store.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import { expect } from 'chai'
  2. import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
  3. import sinon from 'sinon'
  4. import customLocalStorage from '@/infrastructure/local-storage'
  5. function waitForWatchers(callback: () => void) {
  6. return new Promise(resolve => {
  7. callback()
  8. window.setTimeout(resolve, 1)
  9. })
  10. }
  11. describe('ReactScopeValueStore', function () {
  12. it('can set and retrieve a value', function () {
  13. const store = new ReactScopeValueStore()
  14. store.set('test', 'wombat')
  15. const retrieved = store.get('test')
  16. expect(retrieved).to.equal('wombat')
  17. })
  18. it('can overwrite a value', function () {
  19. const store = new ReactScopeValueStore()
  20. store.set('test', 'wombat')
  21. store.set('test', 'not a wombat')
  22. const retrieved = store.get('test')
  23. expect(retrieved).to.equal('not a wombat')
  24. })
  25. it('can overwrite a nested value', function () {
  26. const store = new ReactScopeValueStore()
  27. store.set('test', { prop: 'wombat' })
  28. store.set('test.prop', 'not a wombat')
  29. const retrieved = store.get('test.prop')
  30. expect(retrieved).to.equal('not a wombat')
  31. })
  32. it('throws an error when retrieving an unknown value', function () {
  33. const store = new ReactScopeValueStore()
  34. expect(() => store.get('test')).to.throw
  35. })
  36. it('can watch a value', async function () {
  37. const store = new ReactScopeValueStore()
  38. store.set('changing', 'one')
  39. store.set('fixed', 'one')
  40. const changingItemWatcher = sinon.stub()
  41. const fixedItemWatcher = sinon.stub()
  42. await waitForWatchers(() => {
  43. store.watch('changing', changingItemWatcher)
  44. store.watch('fixed', fixedItemWatcher)
  45. })
  46. expect(changingItemWatcher).to.have.been.calledWith('one')
  47. expect(fixedItemWatcher).to.have.been.calledWith('one')
  48. changingItemWatcher.reset()
  49. fixedItemWatcher.reset()
  50. await waitForWatchers(() => {
  51. store.set('changing', 'two')
  52. })
  53. expect(changingItemWatcher).to.have.been.calledWith('two')
  54. expect(fixedItemWatcher).not.to.have.been.called
  55. })
  56. it('allows synchronous watcher updates', function () {
  57. const store = new ReactScopeValueStore()
  58. store.set('test', 'wombat')
  59. const watcher = sinon.stub()
  60. store.watch('test', watcher)
  61. store.set('test', 'not a wombat')
  62. expect(watcher).not.to.have.been.called
  63. store.flushUpdates()
  64. expect(watcher).to.have.been.calledWith('not a wombat')
  65. })
  66. it('removes a watcher', async function () {
  67. const store = new ReactScopeValueStore()
  68. store.set('test', 'wombat')
  69. const watcher = sinon.stub()
  70. const removeWatcher = store.watch('test', watcher)
  71. store.flushUpdates()
  72. watcher.reset()
  73. removeWatcher()
  74. store.set('test', 'not a wombat')
  75. store.flushUpdates()
  76. expect(watcher).not.to.have.been.called
  77. })
  78. it('does not call a watcher removed between observing change and being called', async function () {
  79. const store = new ReactScopeValueStore()
  80. store.set('test', 'wombat')
  81. const watcher = sinon.stub()
  82. const removeWatcher = store.watch('test', watcher)
  83. store.flushUpdates()
  84. watcher.reset()
  85. store.set('test', 'not a wombat')
  86. removeWatcher()
  87. store.flushUpdates()
  88. expect(watcher).not.to.have.been.called
  89. })
  90. it('does not trigger watcher on setting to an identical value', async function () {
  91. const store = new ReactScopeValueStore()
  92. store.set('test', 'wombat')
  93. const watcher = sinon.stub()
  94. await waitForWatchers(() => {
  95. store.watch('test', watcher)
  96. })
  97. expect(watcher).to.have.been.calledWith('wombat')
  98. watcher.reset()
  99. await waitForWatchers(() => {
  100. store.set('test', 'wombat')
  101. })
  102. expect(watcher).not.to.have.been.called
  103. })
  104. it('can watch a value before it has been set', async function () {
  105. const store = new ReactScopeValueStore()
  106. const watcher = sinon.stub()
  107. store.watch('test', watcher)
  108. await waitForWatchers(() => {
  109. store.set('test', 'wombat')
  110. })
  111. expect(watcher).to.have.been.calledWith('wombat')
  112. })
  113. it('throws an error when watching an unknown value', function () {
  114. const store = new ReactScopeValueStore()
  115. expect(() => store.watch('test', () => {})).to.throw
  116. })
  117. it('sets nested value if watched', function () {
  118. const store = new ReactScopeValueStore()
  119. store.set('test', { nested: 'one' })
  120. const watcher = sinon.stub()
  121. store.watch('test.nested', watcher)
  122. const retrieved = store.get('test.nested')
  123. expect(retrieved).to.equal('one')
  124. })
  125. it('does not set nested value if not watched', function () {
  126. const store = new ReactScopeValueStore()
  127. store.set('test', { nested: 'one' })
  128. expect(() => store.get('test.nested')).to.throw
  129. })
  130. it('can watch a nested value', async function () {
  131. const store = new ReactScopeValueStore()
  132. store.set('test', { nested: 'one' })
  133. const watcher = sinon.stub()
  134. store.watch('test.nested', watcher)
  135. await waitForWatchers(() => {
  136. store.set('test', { nested: 'two' })
  137. })
  138. expect(watcher).to.have.been.calledWith('two')
  139. })
  140. it('can watch a deeply nested value', async function () {
  141. const store = new ReactScopeValueStore()
  142. store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
  143. const watcher = sinon.stub()
  144. store.watch('test.levelOne.levelTwo.levelThree', watcher)
  145. await waitForWatchers(() => {
  146. store.set('test', { levelOne: { levelTwo: { levelThree: 'two' } } })
  147. })
  148. expect(watcher).to.have.been.calledWith('two')
  149. })
  150. it('does not inform nested value watcher when nested value does not change', async function () {
  151. const store = new ReactScopeValueStore()
  152. store.set('test', { nestedOne: 'one', nestedTwo: 'one' })
  153. const nestedOneWatcher = sinon.stub()
  154. const nestedTwoWatcher = sinon.stub()
  155. await waitForWatchers(() => {
  156. store.watch('test.nestedOne', nestedOneWatcher)
  157. store.watch('test.nestedTwo', nestedTwoWatcher)
  158. })
  159. nestedOneWatcher.reset()
  160. nestedTwoWatcher.reset()
  161. await waitForWatchers(() => {
  162. store.set('test', { nestedOne: 'two', nestedTwo: 'one' })
  163. })
  164. expect(nestedOneWatcher).to.have.been.calledWith('two')
  165. expect(nestedTwoWatcher).not.to.have.been.called
  166. })
  167. it('deletes nested values that no longer exist', function () {
  168. const store = new ReactScopeValueStore()
  169. store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
  170. store.set('test', { levelOne: { different: 'wombat' } })
  171. const retrieved = store.get('test.levelOne.different')
  172. expect(retrieved).to.equal('wombat')
  173. expect(() => store.get('test.levelOne.levelTwo')).to.throw
  174. expect(() => store.get('test.levelOne.levelTwo.levelThree')).to.throw
  175. })
  176. it('does not throw for allowed non-existent path', function () {
  177. const store = new ReactScopeValueStore()
  178. store.allowNonExistentPath('wombat')
  179. store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
  180. store.set('test', { levelOne: { different: 'wombat' } })
  181. expect(() => store.get('test')).not.to.throw
  182. expect(store.get('wombat')).to.equal(undefined)
  183. })
  184. it('does not throw for deep allowed non-existent path', function () {
  185. const store = new ReactScopeValueStore()
  186. store.allowNonExistentPath('wombat', true)
  187. expect(() => store.get('wombat')).not.to.throw
  188. expect(() => store.get('wombat.nested')).not.to.throw
  189. expect(() => store.get('wombat.really.very.nested')).not.to.throw
  190. })
  191. it('throws for nested value in non-deep allowed non-existent path', function () {
  192. const store = new ReactScopeValueStore()
  193. store.allowNonExistentPath('wombat', false)
  194. expect(() => store.get('wombat.nested')).to.throw
  195. })
  196. it('throws for ancestor of allowed non-existent path', function () {
  197. const store = new ReactScopeValueStore()
  198. store.allowNonExistentPath('wombat.nested', true)
  199. expect(() => store.get('wombat.really.very.nested')).not.to.throw
  200. expect(() => store.get('wombat')).to.throw
  201. })
  202. it('updates ancestors', async function () {
  203. const store = new ReactScopeValueStore()
  204. const testValue = {
  205. prop1: {
  206. subProp: 'wombat',
  207. },
  208. prop2: {
  209. subProp: 'wombat',
  210. },
  211. }
  212. store.set('test', testValue)
  213. const rootWatcher = sinon.stub()
  214. const prop1Watcher = sinon.stub()
  215. const subPropWatcher = sinon.stub()
  216. const prop2Watcher = sinon.stub()
  217. await waitForWatchers(() => {
  218. store.watch('test', rootWatcher)
  219. store.watch('test.prop1', prop1Watcher)
  220. store.watch('test.prop1.subProp', subPropWatcher)
  221. store.watch('test.prop2', prop2Watcher)
  222. })
  223. rootWatcher.reset()
  224. prop1Watcher.reset()
  225. subPropWatcher.reset()
  226. prop2Watcher.reset()
  227. await waitForWatchers(() => {
  228. store.set('test.prop1.subProp', 'picard')
  229. })
  230. expect(store.get('test')).to.deep.equal({
  231. prop1: {
  232. subProp: 'picard',
  233. },
  234. prop2: {
  235. subProp: 'wombat',
  236. },
  237. })
  238. expect(store.get('test.prop2')).to.equal(testValue.prop2)
  239. expect(rootWatcher).to.have.been.called
  240. expect(prop1Watcher).to.have.been.called
  241. expect(subPropWatcher).to.have.been.called
  242. expect(prop2Watcher).not.to.have.been.called
  243. })
  244. describe('persistence', function () {
  245. beforeEach(function () {
  246. customLocalStorage.clear()
  247. })
  248. it('persists string to local storage', function () {
  249. const store = new ReactScopeValueStore()
  250. store.persisted('test-path', 'fallback value', 'test-storage-key')
  251. expect(store.get('test-path')).to.equal('fallback value')
  252. store.set('test-path', 'new value')
  253. expect(customLocalStorage.getItem('test-storage-key')).to.equal(
  254. 'new value'
  255. )
  256. })
  257. it("doesn't persist string to local storage until set() is called", function () {
  258. const store = new ReactScopeValueStore()
  259. store.persisted('test-path', 'fallback value', 'test-storage-key')
  260. expect(customLocalStorage.getItem('test-storage-key')).to.equal(null)
  261. })
  262. it('converts persisted value', function () {
  263. const store = new ReactScopeValueStore()
  264. store.persisted('test-path', false, 'test-storage-key', {
  265. toPersisted: value => (value ? 'on' : 'off'),
  266. fromPersisted: persistedValue => persistedValue === 'on',
  267. })
  268. store.set('test-path', true)
  269. expect(customLocalStorage.getItem('test-storage-key')).to.equal('on')
  270. })
  271. })
  272. })