blob: 01d39d123140887ef1e5ba85f336b8c921043d4a [file] [log] [blame]
// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Draft} from 'immer';
import {createStore, StoreError} from './store';
import {using} from '../base/disposable';
interface NestedState {
value: number;
}
interface Foo {
counter: number;
nested: NestedState;
}
interface State {
foo: Foo;
}
const initialState: State = {
foo: {
counter: 0,
nested: {
value: 42,
},
},
};
describe('store', () => {
it('can initialise', () => {
const store = createStore(initialState);
expect(store.state).toEqual(initialState);
});
test('old state !== state after edits', () => {
const store = createStore(initialState);
store.edit((draft) => {
draft.foo.nested.value = 88;
});
expect(store.state).not.toEqual(initialState);
});
it('can edit', () => {
const store = createStore(initialState);
store.edit((draft) => {
draft.foo.counter += 1;
});
expect(store.state).toEqual({
...initialState,
foo: {
...initialState.foo,
counter: 1,
},
});
});
it('can take multiple edits', () => {
const store = createStore(initialState);
const callback = jest.fn();
store.subscribe(callback);
store.edit([
(draft) => {
draft.foo.counter += 1;
},
(draft) => {
draft.foo.nested.value += 1;
},
]);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(
{
foo: {
counter: 1,
nested: {
value: 43,
},
},
},
initialState);
expect(store.state).toEqual({
foo: {
counter: 1,
nested: {
value: 43,
},
},
});
});
it('can support a huge number of edits', () => {
const store = createStore(initialState);
const N = 100_000;
const edits = Array(N).fill((draft: Draft<State>) => {
draft.foo.counter++;
});
store.edit(edits);
expect(store.state.foo.counter).toEqual(N);
});
it('notifies subscribers', () => {
const store = createStore(initialState);
const callback = jest.fn();
store.subscribe(callback);
store.edit((draft) => {
draft.foo.counter += 1;
});
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(
{
foo: {
...initialState.foo,
counter: 1,
},
},
initialState);
});
it('does not notify unsubscribed subscribers', () => {
const store = createStore(initialState);
const callback = jest.fn();
// Subscribe then immediately unsubscribe
using(store.subscribe(callback));
// Make an arbitrary edit
store.edit((draft) => {
draft.foo.counter += 1;
});
expect(callback).not.toHaveBeenCalled();
});
});
describe('proxy store', () => {
it('can initialise and edit', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
fooState.edit((draft) => {
draft.counter += 1;
});
expect(fooState.state).toEqual({
counter: 1,
nested: {
value: 42,
},
});
});
it('can create more proxies and edit', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const nestedStore = fooState.createProxy<NestedState>(['nested']);
nestedStore.edit((draft) => {
draft.value += 1;
});
expect(nestedStore.state).toEqual({
value: 43,
});
});
it('notifies subscribers', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
fooState.subscribe(callback);
store.edit((draft) => {
draft.foo.counter += 1;
});
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(
{
...initialState.foo,
counter: 1,
},
initialState.foo);
});
it('does not notify unsubscribed subscribers', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
// Subscribe then immediately unsubscribe
using(store.subscribe(callback));
// Make an arbitrary edit
fooState.edit((draft) => {
draft.counter += 1;
});
expect(callback).not.toHaveBeenCalled();
});
it('throws on state access when path doesn\'t exist', () => {
const store = createStore(initialState);
// This path is incorrect - baz doesn't exist in State
const fooStore = store.createProxy<Foo>(['baz']);
expect(() => {
fooStore.state;
}).toThrow(StoreError);
});
it('throws on edit when path doesn\'t exist', () => {
const store = createStore(initialState);
// This path is incorrect - baz doesn't exist in State
const fooState = store.createProxy<Foo>(['baz']);
expect(() => {
fooState.edit((draft) => {
draft.counter += 1;
});
}).toThrow(StoreError);
});
it('notifies when relevant edits are made from root store', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
// Subscribe on the proxy store
fooState.subscribe(callback);
// Edit the root store
store.edit((draft) => {
draft.foo.counter++;
});
// Expect proxy callback called with correct subtree
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith(
{
...initialState.foo,
counter: 1,
},
initialState.foo);
});
it('ignores irrrelevant edits from the root store', () => {
const store = createStore(initialState);
const nestedStore = store.createProxy<NestedState>(['foo', 'nested']);
const callback = jest.fn();
// Subscribe on the proxy store
nestedStore.subscribe(callback);
// Edit an irrelevant subtree on the root store
store.edit((draft) => {
draft.foo.counter++;
});
// Ensure proxy callback hasn't been called
expect(callback).not.toHaveBeenCalled();
});
it('notifies subscribers', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
fooState.subscribe(callback);
store.edit((draft) => {
draft.foo.counter += 1;
});
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(
{
...initialState.foo,
counter: 1,
},
initialState.foo);
});
it('does not notify unsubscribed subscribers', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
// Subscribe then immediately unsubscribe
fooState.subscribe(callback).dispose();
// Make an arbitrary edit
fooState.edit((draft) => {
draft.counter += 1;
});
expect(callback).not.toHaveBeenCalled();
});
it('throws on state access when path doesn\'t exist', () => {
const store = createStore(initialState);
// This path is incorrect - baz doesn't exist in State
const fooStore = store.createProxy<Foo>(['baz']);
expect(() => {
fooStore.state;
}).toThrow(StoreError);
});
it('throws on edit when path doesn\'t exist', () => {
const store = createStore(initialState);
// This path is incorrect - baz doesn't exist in State
const fooState = store.createProxy<Foo>(['baz']);
expect(() => {
fooState.edit((draft) => {
draft.counter += 1;
});
}).toThrow(StoreError);
});
it('notifies when relevant edits are made from root store', () => {
const store = createStore(initialState);
const fooState = store.createProxy<Foo>(['foo']);
const callback = jest.fn();
// Subscribe on the proxy store
fooState.subscribe(callback);
// Edit the root store
store.edit((draft) => {
draft.foo.counter++;
});
// Expect proxy callback called with correct subtree
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith(
{
...initialState.foo,
counter: 1,
},
initialState.foo);
});
it('ignores irrrelevant edits from the root store', () => {
const store = createStore(initialState);
const nestedStore = store.createProxy<NestedState>(['foo', 'nested']);
const callback = jest.fn();
// Subscribe on the proxy store
nestedStore.subscribe(callback);
// Edit an irrelevant subtree on the root store
store.edit((draft) => {
draft.foo.counter++;
});
// Ensure proxy callback hasn't been called
expect(callback).not.toHaveBeenCalled();
});
});