|
|
@@ -0,0 +1,299 @@
|
|
|
+import { expect } from 'chai'
|
|
|
+import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
|
|
|
+import sinon from 'sinon'
|
|
|
+import customLocalStorage from '@/infrastructure/local-storage'
|
|
|
+
|
|
|
+function waitForWatchers(callback: () => void) {
|
|
|
+ return new Promise(resolve => {
|
|
|
+ callback()
|
|
|
+ window.setTimeout(resolve, 1)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+describe('ReactScopeValueStore', function () {
|
|
|
+ it('can set and retrieve a value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ const retrieved = store.get('test')
|
|
|
+ expect(retrieved).to.equal('wombat')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can overwrite a value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ store.set('test', 'not a wombat')
|
|
|
+ const retrieved = store.get('test')
|
|
|
+ expect(retrieved).to.equal('not a wombat')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can overwrite a nested value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { prop: 'wombat' })
|
|
|
+ store.set('test.prop', 'not a wombat')
|
|
|
+ const retrieved = store.get('test.prop')
|
|
|
+ expect(retrieved).to.equal('not a wombat')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('throws an error when retrieving an unknown value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ expect(() => store.get('test')).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can watch a value', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('changing', 'one')
|
|
|
+ store.set('fixed', 'one')
|
|
|
+ const changingItemWatcher = sinon.stub()
|
|
|
+ const fixedItemWatcher = sinon.stub()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.watch('changing', changingItemWatcher)
|
|
|
+ store.watch('fixed', fixedItemWatcher)
|
|
|
+ })
|
|
|
+ expect(changingItemWatcher).to.have.been.calledWith('one')
|
|
|
+ expect(fixedItemWatcher).to.have.been.calledWith('one')
|
|
|
+ changingItemWatcher.reset()
|
|
|
+ fixedItemWatcher.reset()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('changing', 'two')
|
|
|
+ })
|
|
|
+ expect(changingItemWatcher).to.have.been.calledWith('two')
|
|
|
+ expect(fixedItemWatcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ it('allows synchronous watcher updates', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ store.watch('test', watcher)
|
|
|
+ store.set('test', 'not a wombat')
|
|
|
+ expect(watcher).not.to.have.been.called
|
|
|
+ store.flushUpdates()
|
|
|
+ expect(watcher).to.have.been.calledWith('not a wombat')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('removes a watcher', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ const removeWatcher = store.watch('test', watcher)
|
|
|
+ store.flushUpdates()
|
|
|
+ watcher.reset()
|
|
|
+ removeWatcher()
|
|
|
+ store.set('test', 'not a wombat')
|
|
|
+ store.flushUpdates()
|
|
|
+ expect(watcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not call a watcher removed between observing change and being called', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ const removeWatcher = store.watch('test', watcher)
|
|
|
+ store.flushUpdates()
|
|
|
+ watcher.reset()
|
|
|
+ store.set('test', 'not a wombat')
|
|
|
+ removeWatcher()
|
|
|
+ store.flushUpdates()
|
|
|
+ expect(watcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not trigger watcher on setting to an identical value', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.watch('test', watcher)
|
|
|
+ })
|
|
|
+ expect(watcher).to.have.been.calledWith('wombat')
|
|
|
+ watcher.reset()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ })
|
|
|
+ expect(watcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can watch a value before it has been set', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ store.watch('test', watcher)
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test', 'wombat')
|
|
|
+ })
|
|
|
+ expect(watcher).to.have.been.calledWith('wombat')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('throws an error when watching an unknown value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ expect(() => store.watch('test', () => {})).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('sets nested value if watched', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { nested: 'one' })
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ store.watch('test.nested', watcher)
|
|
|
+ const retrieved = store.get('test.nested')
|
|
|
+ expect(retrieved).to.equal('one')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not set nested value if not watched', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { nested: 'one' })
|
|
|
+ expect(() => store.get('test.nested')).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can watch a nested value', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { nested: 'one' })
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ store.watch('test.nested', watcher)
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test', { nested: 'two' })
|
|
|
+ })
|
|
|
+ expect(watcher).to.have.been.calledWith('two')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('can watch a deeply nested value', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
|
|
|
+ const watcher = sinon.stub()
|
|
|
+ store.watch('test.levelOne.levelTwo.levelThree', watcher)
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test', { levelOne: { levelTwo: { levelThree: 'two' } } })
|
|
|
+ })
|
|
|
+ expect(watcher).to.have.been.calledWith('two')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not inform nested value watcher when nested value does not change', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { nestedOne: 'one', nestedTwo: 'one' })
|
|
|
+ const nestedOneWatcher = sinon.stub()
|
|
|
+ const nestedTwoWatcher = sinon.stub()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.watch('test.nestedOne', nestedOneWatcher)
|
|
|
+ store.watch('test.nestedTwo', nestedTwoWatcher)
|
|
|
+ })
|
|
|
+ nestedOneWatcher.reset()
|
|
|
+ nestedTwoWatcher.reset()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test', { nestedOne: 'two', nestedTwo: 'one' })
|
|
|
+ })
|
|
|
+ expect(nestedOneWatcher).to.have.been.calledWith('two')
|
|
|
+ expect(nestedTwoWatcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ it('deletes nested values that no longer exist', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
|
|
|
+ store.set('test', { levelOne: { different: 'wombat' } })
|
|
|
+ const retrieved = store.get('test.levelOne.different')
|
|
|
+ expect(retrieved).to.equal('wombat')
|
|
|
+ expect(() => store.get('test.levelOne.levelTwo')).to.throw
|
|
|
+ expect(() => store.get('test.levelOne.levelTwo.levelThree')).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not throw for allowed non-existent path', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.allowNonExistentPath('wombat')
|
|
|
+ store.set('test', { levelOne: { levelTwo: { levelThree: 'one' } } })
|
|
|
+ store.set('test', { levelOne: { different: 'wombat' } })
|
|
|
+ expect(() => store.get('test')).not.to.throw
|
|
|
+ expect(store.get('wombat')).to.equal(undefined)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('does not throw for deep allowed non-existent path', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.allowNonExistentPath('wombat', true)
|
|
|
+ expect(() => store.get('wombat')).not.to.throw
|
|
|
+ expect(() => store.get('wombat.nested')).not.to.throw
|
|
|
+ expect(() => store.get('wombat.really.very.nested')).not.to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('throws for nested value in non-deep allowed non-existent path', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.allowNonExistentPath('wombat', false)
|
|
|
+ expect(() => store.get('wombat.nested')).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('throws for ancestor of allowed non-existent path', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.allowNonExistentPath('wombat.nested', true)
|
|
|
+ expect(() => store.get('wombat.really.very.nested')).not.to.throw
|
|
|
+ expect(() => store.get('wombat')).to.throw
|
|
|
+ })
|
|
|
+
|
|
|
+ it('updates ancestors', async function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ const testValue = {
|
|
|
+ prop1: {
|
|
|
+ subProp: 'wombat',
|
|
|
+ },
|
|
|
+ prop2: {
|
|
|
+ subProp: 'wombat',
|
|
|
+ },
|
|
|
+ }
|
|
|
+ store.set('test', testValue)
|
|
|
+ const rootWatcher = sinon.stub()
|
|
|
+ const prop1Watcher = sinon.stub()
|
|
|
+ const subPropWatcher = sinon.stub()
|
|
|
+ const prop2Watcher = sinon.stub()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.watch('test', rootWatcher)
|
|
|
+ store.watch('test.prop1', prop1Watcher)
|
|
|
+ store.watch('test.prop1.subProp', subPropWatcher)
|
|
|
+ store.watch('test.prop2', prop2Watcher)
|
|
|
+ })
|
|
|
+ rootWatcher.reset()
|
|
|
+ prop1Watcher.reset()
|
|
|
+ subPropWatcher.reset()
|
|
|
+ prop2Watcher.reset()
|
|
|
+ await waitForWatchers(() => {
|
|
|
+ store.set('test.prop1.subProp', 'picard')
|
|
|
+ })
|
|
|
+ expect(store.get('test')).to.deep.equal({
|
|
|
+ prop1: {
|
|
|
+ subProp: 'picard',
|
|
|
+ },
|
|
|
+ prop2: {
|
|
|
+ subProp: 'wombat',
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(store.get('test.prop2')).to.equal(testValue.prop2)
|
|
|
+ expect(rootWatcher).to.have.been.called
|
|
|
+ expect(prop1Watcher).to.have.been.called
|
|
|
+ expect(subPropWatcher).to.have.been.called
|
|
|
+ expect(prop2Watcher).not.to.have.been.called
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('persistence', function () {
|
|
|
+ beforeEach(function () {
|
|
|
+ customLocalStorage.clear()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('persists string to local storage', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.persisted('test-path', 'fallback value', 'test-storage-key')
|
|
|
+ expect(store.get('test-path')).to.equal('fallback value')
|
|
|
+ store.set('test-path', 'new value')
|
|
|
+ expect(customLocalStorage.getItem('test-storage-key')).to.equal(
|
|
|
+ 'new value'
|
|
|
+ )
|
|
|
+ })
|
|
|
+
|
|
|
+ it("doesn't persist string to local storage until set() is called", function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.persisted('test-path', 'fallback value', 'test-storage-key')
|
|
|
+ expect(customLocalStorage.getItem('test-storage-key')).to.equal(null)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('converts persisted value', function () {
|
|
|
+ const store = new ReactScopeValueStore()
|
|
|
+ store.persisted('test-path', false, 'test-storage-key', {
|
|
|
+ toPersisted: value => (value ? 'on' : 'off'),
|
|
|
+ fromPersisted: persistedValue => persistedValue === 'on',
|
|
|
+ })
|
|
|
+ store.set('test-path', true)
|
|
|
+ expect(customLocalStorage.getItem('test-storage-key')).to.equal('on')
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|