Add more tests to dataverse
This commit is contained in:
parent
e3a9d7665a
commit
a0c7255157
6 changed files with 421 additions and 7 deletions
|
@ -150,14 +150,20 @@ export default class Atom<State> implements IdentityPrismProvider {
|
|||
this._checkUpdates(this._rootScope, oldState, newState)
|
||||
}
|
||||
|
||||
get() {
|
||||
get(): State {
|
||||
return this._currentState
|
||||
}
|
||||
|
||||
getByPointer<S>(fn: (p: Pointer<State>) => Pointer<S>): S {
|
||||
const pointer = fn(this.pointer)
|
||||
const path = getPointerParts(pointer).path
|
||||
return this._getIn(path) as S
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of the atom at `path`.
|
||||
*/
|
||||
getIn(path: (string | number)[]): unknown {
|
||||
private _getIn(path: (string | number)[]): unknown {
|
||||
return path.length === 0 ? this.get() : get(this.get(), path)
|
||||
}
|
||||
|
||||
|
@ -230,7 +236,7 @@ export default class Atom<State> implements IdentityPrismProvider {
|
|||
const subscribe = (listener: (val: unknown) => void) =>
|
||||
this._onPathValueChange(path, listener)
|
||||
|
||||
const getValue = () => this.getIn(path)
|
||||
const getValue = () => this._getIn(path)
|
||||
|
||||
return prism(() => {
|
||||
return prism.source(subscribe, getValue)
|
||||
|
|
79
packages/dataverse/src/Ticker.test.ts
Normal file
79
packages/dataverse/src/Ticker.test.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import {Ticker} from '@theatre/dataverse'
|
||||
import {EMPTY_TICKS_BEFORE_GOING_DORMANT} from './Ticker'
|
||||
|
||||
describe(`Ticker`, () => {
|
||||
test(`Usage of Ticker`, async () => {
|
||||
const ticker = new Ticker()
|
||||
const listener = jest.fn()
|
||||
|
||||
ticker.onNextTick(listener)
|
||||
|
||||
ticker.tick()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
|
||||
ticker.tick()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test(`Tickers go dormant`, () => {
|
||||
const onDormant = jest.fn()
|
||||
const onActive = jest.fn()
|
||||
const ticker = new Ticker({onDormant, onActive})
|
||||
expect(ticker.dormant).toBe(true)
|
||||
|
||||
const listener = jest.fn()
|
||||
ticker.onNextTick(listener)
|
||||
expect(ticker.dormant).toBe(false)
|
||||
expect(onActive).toHaveBeenCalledTimes(1)
|
||||
|
||||
ticker.tick()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
|
||||
// at this point, the ticker is active, but after a few ticks, it should go dormant
|
||||
for (let i = 0; i < EMPTY_TICKS_BEFORE_GOING_DORMANT; i++) {
|
||||
ticker.tick()
|
||||
if (i < EMPTY_TICKS_BEFORE_GOING_DORMANT - 1) {
|
||||
expect(ticker.dormant).toBe(false)
|
||||
expect(onDormant).toHaveBeenCalledTimes(0)
|
||||
}
|
||||
}
|
||||
|
||||
expect(ticker.dormant).toBe(true)
|
||||
expect(onDormant).toHaveBeenCalledTimes(1)
|
||||
|
||||
// after going dormant, it should stay dormant
|
||||
ticker.tick()
|
||||
expect(ticker.dormant).toBe(true)
|
||||
expect(onDormant).toHaveBeenCalledTimes(1)
|
||||
|
||||
// but if we schedule a callback, it should go active again
|
||||
ticker.onNextTick(listener)
|
||||
expect(ticker.dormant).toBe(false)
|
||||
expect(onActive).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
test(`Ticker.onThisOrNextTick()`, () => {
|
||||
const ticker = new Ticker()
|
||||
const mainListener = jest.fn(() => {
|
||||
ticker.onNextTick(thisWillBeCalledOnTheNextTick)
|
||||
ticker.onThisOrNextTick(thisWillBeCalledOnTheSameTick)
|
||||
})
|
||||
const thisWillBeCalledOnTheSameTick = jest.fn()
|
||||
const thisWillBeCalledOnTheNextTick = jest.fn()
|
||||
|
||||
ticker.onThisOrNextTick(mainListener)
|
||||
expect(mainListener).toHaveBeenCalledTimes(0)
|
||||
expect(thisWillBeCalledOnTheSameTick).toHaveBeenCalledTimes(0)
|
||||
expect(thisWillBeCalledOnTheNextTick).toHaveBeenCalledTimes(0)
|
||||
|
||||
ticker.tick()
|
||||
expect(mainListener).toHaveBeenCalledTimes(1)
|
||||
expect(thisWillBeCalledOnTheSameTick).toHaveBeenCalledTimes(1)
|
||||
expect(thisWillBeCalledOnTheNextTick).toHaveBeenCalledTimes(0)
|
||||
|
||||
ticker.tick()
|
||||
expect(mainListener).toHaveBeenCalledTimes(1)
|
||||
expect(thisWillBeCalledOnTheSameTick).toHaveBeenCalledTimes(1)
|
||||
expect(thisWillBeCalledOnTheNextTick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
|
@ -6,7 +6,7 @@ type ICallback = (t: number) => void
|
|||
*
|
||||
* Perhaps counting ticks vs. time is not the best way to do this. But it's a start.
|
||||
*/
|
||||
const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant
|
||||
export const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant
|
||||
|
||||
/**
|
||||
* The Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an
|
||||
|
|
|
@ -5,7 +5,7 @@ import Atom, {val} from './Atom'
|
|||
import prism from './prism/prism'
|
||||
import Ticker from './Ticker'
|
||||
|
||||
describe(`v2 atom`, () => {
|
||||
describe(`integration`, () => {
|
||||
describe(`identity pointers`, () => {
|
||||
it(`should work`, () => {
|
||||
const data = {foo: 'hi', bar: 0}
|
||||
|
|
329
packages/dataverse/src/prism/newPrism.test.ts
Normal file
329
packages/dataverse/src/prism/newPrism.test.ts
Normal file
|
@ -0,0 +1,329 @@
|
|||
import {prism} from '@theatre/dataverse'
|
||||
|
||||
describe(`prism()`, () => {
|
||||
// these tests are going will act as a guide on how to use prisms.
|
||||
|
||||
// prisms are a way to create a value that depends on other values.
|
||||
// prisms can be hot or cold, they have dependencies and dependents, and hot prisms can be stale or fresh.
|
||||
|
||||
// let's start with a simple example:
|
||||
test(`using a pretty useless prism`, async () => {
|
||||
// each prism has a calculate function that it runs to calculate its value. let's make a simple function that just returns 1
|
||||
const calculate = jest.fn(() => 1)
|
||||
|
||||
// now we can make a prism out of it
|
||||
const pr = prism(calculate)
|
||||
|
||||
// now, this prism is pretty useless. it doesn't depend on anything, and it doesn't have any dependents.
|
||||
// but we can already illustrate some of the concepts that are important to understand prisms.
|
||||
|
||||
// `calculate` won't be called until it's needed
|
||||
expect(calculate).not.toHaveBeenCalled()
|
||||
|
||||
// we can get the value of the prism, which will be what `calculate` returned
|
||||
expect(pr.getValue()).toBe(1)
|
||||
|
||||
// now the calculate function will have be called
|
||||
expect(calculate).toHaveBeenCalledTimes(1)
|
||||
|
||||
// now, you might expect that if we call `getValue()` again, the calculate function won't be called again.
|
||||
// but that's not the case. the calculate function will be called again, because the prism is cold.
|
||||
// we'll talk about cold/hot in a bit, but let's just say that cold prisms are calculated every time they're read.
|
||||
|
||||
pr.getValue()
|
||||
expect(calculate).toHaveBeenCalledTimes(2)
|
||||
|
||||
// we can even check whether a prism is hot or cold. Ours is cold.
|
||||
expect(pr.isHot).toBe(false)
|
||||
|
||||
// we'll get to hot prisms soon, but let's talk about dependencies and dependents first.
|
||||
})
|
||||
|
||||
// prisms can depend on other prisms. let's make a prism that depends on another prism.
|
||||
test(`prisms can depend on other prisms`, async () => {
|
||||
const calculateA = jest.fn(() => 1)
|
||||
const a = prism(calculateA)
|
||||
|
||||
const calculateATimesTwo = jest.fn(() => a.getValue() * 2)
|
||||
const aTimesTwo = prism(calculateATimesTwo)
|
||||
|
||||
// clear the count of mocks
|
||||
function clearMocks() {
|
||||
calculateA.mockClear()
|
||||
calculateATimesTwo.mockClear()
|
||||
}
|
||||
|
||||
// now, `aTimesTwo` depends on `a`.
|
||||
// In our parlance, `aTimesTwo` is a dependent of `a`, and `a` is a dependency of `aTimesTwo`.
|
||||
|
||||
// now if we read the value of `aTimesTwo`, it will call `calculateATimesTwo`, which will call `calculateA`:
|
||||
expect(aTimesTwo.getValue()).toBe(2)
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// and like we saw in the previous test, if we read the value of `aTimesTwo` again, it will call both of our calculate functions again:
|
||||
aTimesTwo.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// but if we read the value of `a`, it won't call `calculateATimesTwo`:
|
||||
a.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// now let's see what happens if we make our prism hot.
|
||||
|
||||
// one way to do this, is to add an `onStale` listener to our prism.
|
||||
const onStale = jest.fn()
|
||||
const unsubscribe = aTimesTwo.onStale(onStale)
|
||||
|
||||
// as soon as we do this, the prism will become hot.
|
||||
expect(aTimesTwo.isHot).toBe(true)
|
||||
|
||||
// and so will its dependencies:
|
||||
expect(a.isHot).toBe(true)
|
||||
|
||||
// so let's see what happens when we read the value of `aTimesTwo` again:
|
||||
aTimesTwo.getValue()
|
||||
// `calculateATimesTwo` will be called again,
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
// and so will `calculateA`,
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// but if we read `aTimesTwo` again, none of the calculate functions will be called again.
|
||||
aTimesTwo.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
|
||||
expect(calculateA).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// this behavior propogates up the dependency chain, so if we read `a` again, `calculateA` won't be called again,
|
||||
// because its value is already fresh.
|
||||
a.getValue()
|
||||
expect(calculateA).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// at this point, since none of our prisms depend on a prism whose value will change, they'll remain
|
||||
// fresh forever.
|
||||
a.getValue()
|
||||
aTimesTwo.getValue()
|
||||
a.getValue()
|
||||
aTimesTwo.getValue()
|
||||
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
|
||||
expect(calculateA).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// but as soon as we unsubscribe from our `onStale()` listener, the prisms will become cold again.
|
||||
unsubscribe()
|
||||
expect(aTimesTwo.isHot).toBe(false)
|
||||
expect(a.isHot).toBe(false)
|
||||
|
||||
// and they'll return back to their cold behavior.
|
||||
aTimesTwo.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
aTimesTwo.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// now, one more thing before we move on. What will if we make `a` hot, but not `aTimesTwo`?
|
||||
// let's try it out.
|
||||
const unsubcribeFromAOnStale = a.onStale(() => {})
|
||||
// a will go hot
|
||||
expect(a.isHot).toBe(true)
|
||||
// but aTimesTwo will stay cold
|
||||
expect(aTimesTwo.isHot).toBe(false)
|
||||
|
||||
// now let's read the value of `a`
|
||||
a.getValue()
|
||||
|
||||
// `calculateA` will be called
|
||||
expect(calculateA).toHaveBeenCalledTimes(1)
|
||||
// and obviously `calculateATimesTwo` won't be called
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// and if we re-read the value of `a`, `calculateA` won't be called again, becuase `a` is hot and its value is fresh.
|
||||
a.getValue()
|
||||
expect(calculateA).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// but if we read the value of `aTimesTwo`, `calculateATimesTwo` will be called, because `aTimesTwo` is cold.
|
||||
aTimesTwo.getValue()
|
||||
expect(calculateATimesTwo).toHaveBeenCalledTimes(1)
|
||||
// yet `calculateA` won't be called, because `a` is hot and its value is fresh.
|
||||
expect(calculateA).toHaveBeenCalledTimes(0)
|
||||
|
||||
clearMocks()
|
||||
|
||||
// in conclusion, if we make a prism hot, it'll make its dependencies hot too.
|
||||
// if we read the value of a cold prism, it'll call its calculate function, which will
|
||||
// call the calculate functions of its dependencies, and so on.
|
||||
// but if we read the value of a hot prism, it'll only call its calculate function if its value is stale.
|
||||
|
||||
// le'ts wrap up this part by unsubscribing from `a`'s `onStale` listener to not have any lingering listeners.
|
||||
unsubcribeFromAOnStale()
|
||||
})
|
||||
|
||||
// so far, our prisms have not depended on any changing values, so in turn, _their_ values have never changed either.
|
||||
// but what if we want to create a prism that depends on a changing value?
|
||||
// we call those values "sources", and we can create them using the `prism.source()` hook:
|
||||
test('prism.source()', () => {
|
||||
// let's say we want to create a prism that depends on this value:
|
||||
let value = 0
|
||||
|
||||
{
|
||||
// the _wrong_ way to do this, is to create a prism that directly reads this value
|
||||
const p = prism(() => value)
|
||||
|
||||
// this will actually work if the prism is cold:
|
||||
expect(p.getValue()).toBe(0)
|
||||
value = 1
|
||||
expect(p.getValue()).toBe(1)
|
||||
|
||||
// but if we make the prism hot, it'll never update its value, because it's not subscribed to any sources.
|
||||
const unsubscribe = p.onStale(() => {})
|
||||
expect(p.isHot).toBe(true)
|
||||
// on first read, it'll give us the current value of `value`, which is 1.
|
||||
expect(p.getValue()).toBe(1)
|
||||
// but if we change `value` again, the prism won't know
|
||||
value = 2
|
||||
expect(p.getValue()).toBe(1)
|
||||
// and so it'll keep returning the old value.
|
||||
expect(p.getValue()).toBe(1)
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
// so, the _right_ way to do this, is to use the `source` hook:
|
||||
{
|
||||
// the source hook requires a `listen` function, and a `get` function.
|
||||
// let's skip the `listen` function for now, and just focus on the `getValue` function.
|
||||
const listen = jest.fn(() => () => {})
|
||||
// the `getValue` function should return the current value of the source.
|
||||
const get = jest.fn(() => value)
|
||||
|
||||
const p = prism(() => {
|
||||
return prism.source(listen, get) * 2
|
||||
})
|
||||
|
||||
value = 1
|
||||
|
||||
// our prism is cold right now. let's see what happens when we read its value.
|
||||
expect(p.getValue()).toBe(2)
|
||||
// `get` will be called once, because we're reading the value of the source for the first time.
|
||||
expect(get).toHaveBeenCalledTimes(1)
|
||||
// and `listen` won't be called at all
|
||||
expect(listen).toHaveBeenCalledTimes(0)
|
||||
|
||||
get.mockClear()
|
||||
|
||||
// now let's make the prism hot
|
||||
const unsubscribe = p.onStale(() => {})
|
||||
expect(p.isHot).toBe(true)
|
||||
expect(p.getValue()).toBe(2)
|
||||
// `get` will be called again, because we're reading the value of the source for the second time.
|
||||
expect(get).toHaveBeenCalledTimes(1)
|
||||
// and `listen` will be called once, because our prism wants to be notified when the source changes.
|
||||
expect(listen).toHaveBeenCalledTimes(1)
|
||||
|
||||
get.mockClear()
|
||||
listen.mockClear()
|
||||
|
||||
// now, since our `listen` function is a mock, it won't actually do anything,
|
||||
// so the prism still won't know when the source changes.
|
||||
value = 2
|
||||
expect(p.getValue()).toBe(2)
|
||||
// `get` won't be called again, because the source hasn't changed.
|
||||
expect(get).toHaveBeenCalledTimes(0)
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
{
|
||||
let value = 0
|
||||
// now let's implement an actual `listen` function.
|
||||
|
||||
// first, we need to keep track of all the listeners that our source wil have
|
||||
const listeners = new Set<(val: number) => void>()
|
||||
|
||||
// the `listen` function should return an stop function.
|
||||
// the stop function should stop listening to the source.
|
||||
const listen = jest.fn((fn) => {
|
||||
listeners.add(fn)
|
||||
|
||||
return function stop() {
|
||||
listeners.delete(fn)
|
||||
}
|
||||
})
|
||||
|
||||
const get = jest.fn(() => value)
|
||||
|
||||
// and now we need to define a function that will notify all the listeners that the source has changed.
|
||||
const set = (newValue: number) => {
|
||||
if (newValue !== value) {
|
||||
value = newValue
|
||||
listeners.forEach((fn) => fn(value))
|
||||
}
|
||||
}
|
||||
|
||||
// don't worry, there are helpers for this in dataverse. but for now, we'll implement
|
||||
// it manually to understand how it works.
|
||||
|
||||
// now let's create a prism that depends on our source.
|
||||
const p = prism(() => {
|
||||
return prism.source(listen, get) * 2
|
||||
})
|
||||
|
||||
// let's make the prism hot
|
||||
const staleListener = jest.fn()
|
||||
const unsubscribe = p.onStale(staleListener)
|
||||
expect(p.isHot).toBe(true)
|
||||
|
||||
// and let's read its value
|
||||
expect(p.getValue()).toBe(0)
|
||||
// `get` will be called once, because we're reading the value of the source for the first time.
|
||||
expect(get).toHaveBeenCalledTimes(1)
|
||||
// and `listen` will be called once, because our prism wants to be notified when the source changes.
|
||||
expect(listen).toHaveBeenCalledTimes(1)
|
||||
|
||||
get.mockClear()
|
||||
listen.mockClear()
|
||||
|
||||
// now let's change the value of the source
|
||||
set(1)
|
||||
|
||||
// this time, our prism will know that the source has changed, and it'll update its value.
|
||||
expect(p.getValue()).toBe(2)
|
||||
|
||||
// and that's how we can create a prism that depends on a changing value.
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
// in practice, we'll almost never need to use the `source` hook directly,
|
||||
// and we'll never need to provide our own `listen` and `get` functions.
|
||||
// instead, we'll use `Atom`s, which are sources that are already implemented for us.
|
||||
describe(`Basics of Atoms`, () => {
|
||||
// TODO
|
||||
})
|
||||
})
|
|
@ -36,9 +36,9 @@ describe('prism', () => {
|
|||
return bD.getValue()
|
||||
})
|
||||
expect(cD.getValue()).toEqual(2)
|
||||
const untap = cD.keepHot()
|
||||
const unsubscribe = cD.keepHot()
|
||||
expect((cD as $IntentionalAny)._state.handle._dependencies.size).toEqual(1)
|
||||
untap()
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
describe('prism.ref()', () => {
|
||||
|
|
Loading…
Reference in a new issue