stateshot
Version:
💾 Non-aggressive history state management with structure sharing.
269 lines (228 loc) • 6.86 kB
JavaScript
/* global test expect jest */
import { History } from './history'
const getState = () => ({
id: 0,
name: 'root',
children: [
{ id: 1, name: 'a', children: [] },
{ id: 2, name: 'b', children: [] },
{
id: 3,
name: 'c',
children: [
{ id: 4, name: 'd', children: [] },
{ id: 5, name: 'e', children: [] }
]
}
]
})
test('can init history', () => {
const history = new History()
const state = getState()
history.pushSync(state)
expect(history.get()).toEqual(state)
})
test('can supply initial history', () => {
const state = getState()
const history = new History({ initialState: state })
expect(history.get()).toEqual(state)
})
test('has correct undo/redo flag', () => {
const history = new History()
expect(history.hasUndo).toBeFalsy()
expect(history.hasRedo).toBeFalsy()
const state = getState()
history.pushSync(state)
expect(history.hasUndo).toBeFalsy()
expect(history.hasRedo).toBeFalsy()
history.pushSync(state)
expect(history.hasUndo).toBeTruthy()
expect(history.hasRedo).toBeFalsy()
history.undo()
expect(history.hasUndo).toBeFalsy()
expect(history.hasRedo).toBeTruthy()
history.redo()
expect(history.hasUndo).toBeTruthy()
expect(history.hasRedo).toBeFalsy()
})
test('can get state after undo', () => {
const history = new History()
const state = getState()
history.pushSync(state)
state.name = 'x'
history.pushSync(state)
state.name = 'y'
history.pushSync(state)
expect(history.get().name).toEqual('y')
expect(history.undo().get().name).toEqual('x')
expect(history.undo().get().name).toEqual('root')
})
test('can get state with redundant api call', () => {
const history = new History()
const state = getState()
history.pushSync(state)
state.name = 'x'
history.pushSync(state)
state.name = 'y'
history.pushSync(state)
history.undo().undo().undo().undo().undo()
expect(history.get().name).toEqual('root')
expect(history.hasUndo).toBeFalsy()
history.redo().redo().redo().redo().redo()
expect(history.get().name).toEqual('y')
expect(history.hasRedo).toBeFalsy()
})
test('support max length', () => {
const history = new History({ maxLength: 5 })
const state = getState()
for (let i = 0; i < 10; i++) {
history.pushSync(state)
}
expect(history.hasUndo).toBeTruthy()
expect(history.hasRedo).toBeFalsy()
for (let i = 0; i < 5; i++) {
expect(history.$records[i]).toBeNull()
}
for (let i = 0; i < 5; i++) {
history.undo()
}
expect(history.hasUndo).toBeFalsy()
expect(history.hasRedo).toBeTruthy()
})
test('can get valid record length', () => {
const history = new History({ maxLength: 5 })
expect(history.length).toEqual(0)
const state = getState()
history.pushSync(state)
expect(history.length).toEqual(1)
for (let i = 0; i < 4; i++) {
history.pushSync(state)
}
expect(history.length).toEqual(5)
for (let i = 0; i < 4; i++) {
history.pushSync(state)
}
expect(history.length).toEqual(5)
})
test('can clear redo records', () => {
const history = new History()
const state = getState()
for (let i = 0; i < 10; i++) {
history.pushSync(state)
}
for (let i = 0; i < 5; i++) {
history.undo()
}
history.pushSync(state)
for (let i = history.$index + 1; i < 10; i++) {
expect(history.$records[i]).toBeNull()
}
expect(history.hasUndo).toBeTruthy()
expect(history.hasRedo).toBeFalsy()
})
test('support reset', () => {
const history = new History()
const state = getState()
for (let i = 0; i < 10; i++) {
history.pushSync(state)
}
expect(history.$index).toBe(9)
expect(Object.keys(history.$chunks).length).toBeGreaterThan(0)
expect(history.$records.length).toBeGreaterThan(0)
history.reset()
expect(history.$index).toBe(-1)
expect(Object.keys(history.$chunks).length).toBe(0)
expect(history.$records.length).toBe(0)
})
test('return promise with async push', () => {
const history = new History({ delay: 0 })
const state = getState()
return history.push(state).then(h => {
expect(h.get()).toEqual(state)
})
})
test('may reject on async push', () => {
const history = new History({ delay: 0 })
const state = getState()
history.push(state)
history.$debounceTime = null
return expect(history.push(state)).rejects
.toEqual(new Error('Invalid push ops'))
})
test('support async push', () => {
const history = new History({ delay: 5 })
const state = getState()
for (let i = 0; i < 100; i++) {
history.push(state)
}
expect(history.get()).toBeNull()
return new Promise(
(resolve, reject) => setTimeout(resolve, 10)
).then(() => {
expect(history.get()).toEqual(state)
})
})
test('should clear pending state with sync push', () => {
const history = new History({ delay: 5 })
const state = getState()
setTimeout(() => {
state.children[0].id = 100
history.pushSync(state)
}, 0)
return history.push(state).then(() => {
expect(history.length).toEqual(1)
expect(history.get()).toEqual(state)
})
})
test('support pick index', () => {
const history = new History()
const state = getState()
history.pushSync(state)
state.children[0].id = 100
history.pushSync(state, 0)
expect(history.get()).toEqual(state)
})
test('can return wrong output when picking wrong index', () => {
const history = new History()
const state = getState()
history.pushSync(state)
state.children[0].id = 100
history.pushSync(state, 1)
expect(history.get()).not.toEqual(state)
})
test('support change callback', () => {
const onChange = jest.fn(() => {})
const history = new History({ onChange, delay: 0 })
const state = getState()
history.pushSync(state)
expect(onChange.mock.calls.length).toBe(1)
expect(onChange.mock.calls[0][0]).toEqual(state)
const newState = { ...state, name: 'root-changed' }
history.push(newState)
return history.push(newState).then(() => { // simulate calls to be debounced
expect(onChange.mock.calls.length).toBe(2)
history.get(state)
expect(onChange.mock.calls.length).toBe(3)
history.undo().get()
expect(onChange.mock.calls[3][0]).toEqual(state)
history.redo().get()
expect(onChange.mock.calls.length).toBe(5)
expect(onChange.mock.calls[4][0]).toEqual(newState)
})
})
test("change callback isn't fired for initial state", () => {
const onChange = jest.fn(() => {})
const state = getState()
// eslint-disable-next-line no-new
new History({ initialState: state, onChange })
expect(onChange.mock.calls.length).toBe(0)
})
test('can disable chunking', () => {
const history = new History({ useChunks: false })
const state1 = getState()
const state2 = getState()
history.pushSync(state1)
history.pushSync(state2)
expect(history.get()).toEqual(state2)
expect(history.undo().get()).toEqual(state1)
})