| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- 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('handles multiple watchers for the same path added at the same time before the value is set', async function () {
- const store = new ReactScopeValueStore()
- const watcherOne = sinon.stub()
- const watcherTwo = sinon.stub()
- store.watch('test', watcherOne)
- store.watch('test', watcherTwo)
- await waitForWatchers(() => {
- store.set('test', 'wombat')
- })
- expect(watcherOne).to.have.been.calledWith('wombat')
- expect(watcherTwo).to.have.been.calledWith('wombat')
- })
- it('handles multiple watchers for the same path added at the same time after the value is set', async function () {
- const store = new ReactScopeValueStore()
- store.set('test', 'wombat')
- const watcherOne = sinon.stub()
- const watcherTwo = sinon.stub()
- store.watch('test', watcherOne)
- store.watch('test', watcherTwo)
- store.flushUpdates()
- expect(watcherOne).to.have.been.calledWith('wombat')
- expect(watcherTwo).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')
- })
- })
- })
|