packages/__tests__/src/store-v1/store.spec.ts
import { skip } from "rxjs/operators";
import {
LogLevel,
PerformanceMeasurement,
UnregisteredActionError,
ActionRegistrationError,
ReducerNoStateError
} from "@aurelia/store-v1";
import {
createTestStore,
testState,
createStoreWithStateAndOptions,
createCallCounter
} from "./helpers.js";
import { assert } from '@aurelia/testing';
describe("store-v1/store.spec.ts", function () {
this.timeout(100);
it("should accept an initial state", function (done) {
const { initialState, store } = createTestStore();
store.state.subscribe((state) => {
assert.equal(state, initialState);
done();
});
});
it("should fail when dispatching unknown actions", function () {
const { store } = createTestStore();
const unregisteredAction = (currentState: testState, param1: number, param2: number) => {
return { ...currentState, foo: param1 + param2 };
};
return assert.rejects(() => (store.dispatch as any)(unregisteredAction), UnregisteredActionError);
});
it("should fail when dispatching non actions", function () {
const { store } = createTestStore();
return assert.rejects(async () => store.dispatch(undefined as any), UnregisteredActionError);
});
it("should only accept reducers taking at least one parameter", function () {
const { store } = createTestStore();
const fakeAction = () => { /**/ };
assert.throws(() => {
store.registerAction("FakeAction", fakeAction as any);
}, ActionRegistrationError);
});
it("should force reducers to return a new state", function () {
const { store } = createTestStore();
const fakeAction = (_: testState) => { /**/ };
store.registerAction("FakeAction", fakeAction as any);
return assert.rejects(async () => store.dispatch(fakeAction as any), ReducerNoStateError);
});
it("should also accept false and stop queue", async function () {
const { store } = createTestStore();
const { spyObj } = createCallCounter((store as any)._state, "next");
const fakeAction = (_: testState): false => false;
store.registerAction("FakeAction", fakeAction);
await store.dispatch(fakeAction).catch(() => { /**/ });
assert.equal(spyObj.callCounter, 0);
});
it("should also accept async false and stop queue", async function () {
const { store } = createTestStore();
const { spyObj } = createCallCounter((store as any)._state, "next");
const fakeAction = async (_: testState): Promise<false> => Promise.resolve<false>(false);
store.registerAction("FakeAction", fakeAction);
await store.dispatch(fakeAction);
assert.equal(spyObj.callCounter, 0);
});
it("should unregister previously registered actions", async function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => currentState;
store.registerAction("FakeAction", fakeAction);
await store.dispatch(fakeAction);
store.unregisterAction(fakeAction);
await assert.rejects(async () => store.dispatch(fakeAction), UnregisteredActionError);
});
it("should not try to unregister previously unregistered actions", function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => currentState;
try {
store.unregisterAction(fakeAction);
} catch {
assert.fail("threw error instead");
}
});
it("should allow checking for already registered functions via Reducer", function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => currentState;
store.registerAction("FakeAction", fakeAction);
assert.equal(store.isActionRegistered(fakeAction), true);
});
it("should allow checking for already registered functions via previously registered name", function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => currentState;
store.registerAction("FakeAction", fakeAction);
assert.equal(store.isActionRegistered("FakeAction"), true);
});
it("should accept reducers taking multiple parameters", function (done) {
const { store } = createTestStore();
const fakeAction = (currentState: testState, param1: string, param2: string) => {
return { ...currentState, foo: param1 + param2 };
};
store.registerAction("FakeAction", fakeAction as any);
store.dispatch(fakeAction, "A", "B").catch(() => { /**/ });
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.equal(state.foo, "AB");
done();
});
});
it("should queue the next state after dispatching an action", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = (currentState: testState) => {
return { ...currentState, ...modifiedState };
};
store.registerAction("FakeAction", fakeAction);
store.dispatch(fakeAction).catch(() => { /**/ });
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should the previously registered action name as dispatch argument", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = async (_: testState) => Promise.resolve(modifiedState);
const fakeActionRegisteredName = "FakeAction";
store.registerAction(fakeActionRegisteredName, fakeAction);
store.dispatch(fakeActionRegisteredName).catch(() => { /**/ });
// since the async action is coming at a later time we need to skip the initial state
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should support promised actions", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = async (_: testState) => Promise.resolve(modifiedState);
store.registerAction("FakeAction", fakeAction);
store.dispatch(fakeAction).catch(() => { /**/ });
// since the async action is coming at a later time we need to skip the initial state
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should dispatch actions one after another", function (done) {
const { store } = createTestStore();
const actionA = async (currentState: testState) => Promise.resolve({ foo: `${currentState.foo}A` });
const actionB = async (currentState: testState) => Promise.resolve({ foo: `${currentState.foo}B` });
store.registerAction("Action A", actionA);
store.registerAction("Action B", actionB);
store.dispatch(actionA).catch(() => { /**/ });
store.dispatch(actionB).catch(() => { /**/ });
store.state.pipe(
skip(2)
).subscribe((state) => {
assert.equal(state.foo, "barAB");
done();
});
});
it("should maintain queue of execution in concurrency constraints", function () {
const { store } = createTestStore();
createCallCounter((store as any).dispatchQueue, "push");
const { spyObj } = createCallCounter(store, "handleQueue");
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.dispatch(actionA).catch(() => { /**/ });
assert.equal(spyObj.callCounter, 0);
});
it("should log info about dispatched action if turned on via options", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, { logDispatchedActions: true });
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.dispatch(actionA).catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
loggerSpy.reset();
});
it("should log info about dispatched action if turned on via options via custom loglevel", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, {
logDispatchedActions: true,
logDefinitions: {
dispatchedActions: LogLevel.debug
}
});
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.debug);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.dispatch(actionA).catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
loggerSpy.reset();
});
it("should log info about dispatched action and return to default log level if wrong one provided", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, {
logDispatchedActions: true,
logDefinitions: {
dispatchedActions: "foo" as any
}
});
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.dispatch(actionA).catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
loggerSpy.reset();
});
it("should log start-end dispatch duration if turned on via options", async function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(
initialState,
{ measurePerformance: PerformanceMeasurement.StartEnd }
);
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info, false);
const actionA = async (_: testState) => {
return new Promise<testState>((resolve) => {
setTimeout(() => resolve({ foo: "A" }), 1);
});
};
store.registerAction("Action A", actionA);
await store.dispatch(actionA);
assert.typeOf(loggerSpy.lastArgs[0], "string");
assert.equal(Array.isArray(loggerSpy.lastArgs[1]), true);
});
it("should log all dispatch durations if turned on via options", async function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(
initialState,
{ measurePerformance: PerformanceMeasurement.All }
);
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info, false);
const actionA = async (_: testState) => {
return new Promise<testState>((resolve) => {
setTimeout(() => resolve({ foo: "A" }), 1);
});
};
store.registerAction("Action A", actionA);
await store.dispatch(actionA);
assert.typeOf(loggerSpy.lastArgs[0], "string");
assert.equal(Array.isArray(loggerSpy.lastArgs[1]), true);
});
it("should reset the state without going through the internal dispatch queue", async function () {
const { initialState, store } = createTestStore();
const demoAction = (currentState: testState) => {
return { ...currentState, foo: "demo" };
};
store.registerAction("demoAction", demoAction);
await store.dispatch(demoAction);
const { spyObj: internalDispatchSpy } = createCallCounter((store as any), "internalDispatch");
store.resetToState(initialState);
await new Promise<void>((resolve) => store.state.subscribe((state) => {
assert.equal(internalDispatchSpy.callCounter, 0);
assert.equal(state.foo, initialState.foo);
internalDispatchSpy.reset();
resolve();
}));
});
describe("piped dispatch", function () {
it("should fail when dispatching unknown actions", function () {
const { store } = createTestStore();
const unregisteredAction = (currentState: testState, param1: string) => {
return { ...currentState, foo: param1 };
};
assert.throws(() => store.pipe(unregisteredAction, "foo"), UnregisteredActionError);
});
it("should fail when at least one action is unknown", function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => ({ ...currentState });
store.registerAction("FakeAction", fakeAction);
const unregisteredAction = (currentState: testState, param1: string) => ({ ...currentState, foo: param1 });
const pipedDispatch = store.pipe(fakeAction);
assert.throws(() => pipedDispatch.pipe(unregisteredAction, "foo"), UnregisteredActionError);
});
it("should fail when dispatching non actions", function () {
const { store } = createTestStore();
assert.throws(() => store.pipe(undefined as any), UnregisteredActionError);
});
it("should fail when at least one action is no action", function () {
const { store } = createTestStore();
const fakeAction = (currentState: testState) => ({ ...currentState });
store.registerAction("FakeAction", fakeAction);
const pipedDispatch = store.pipe(fakeAction);
assert.throws(() => pipedDispatch.pipe(undefined as any), UnregisteredActionError);
});
it("should force reducer to return a new state", function () {
const { store } = createTestStore();
const fakeAction = (_: testState) => { /**/ };
store.registerAction("FakeAction", fakeAction as any);
return assert.rejects(async () => store.pipe(fakeAction as any).dispatch(), ReducerNoStateError);
});
it("should force all reducers to return a new state", function () {
const { store } = createTestStore();
const fakeActionOk = (currentState: testState) => ({ ...currentState });
const fakeActionNok = (_: testState) => { /**/ };
store.registerAction("FakeActionOk", fakeActionOk);
store.registerAction("FakeActionNok", fakeActionNok as any);
return assert.rejects(
async () => store.pipe(fakeActionNok as any).pipe(fakeActionOk).dispatch(),
ReducerNoStateError
);
});
it("should also accept false and stop queue", function () {
const { store } = createTestStore();
const { spyObj: nextSpy } = createCallCounter((store as any)._state, "next");
const fakeAction = (_: testState): false => false;
store.registerAction("FakeAction", fakeAction);
store.pipe(fakeAction).dispatch().catch(() => { /**/ });
assert.equal(nextSpy.callCounter, 0);
});
it("should also accept async false and stop queue", function () {
const { store } = createTestStore();
const { spyObj: nextSpy } = createCallCounter((store as any)._state, "next");
const fakeAction = async (_: testState): Promise<false> => Promise.resolve<false>(false);
store.registerAction("FakeAction", fakeAction);
store.pipe(fakeAction).dispatch().catch(() => { /**/ });
assert.equal(nextSpy.callCounter, 0);
});
it("should accept reducers taking multiple parameters", function (done) {
const { store } = createTestStore();
const fakeAction = (currentState: testState, param1: string, param2: string) => {
return { ...currentState, foo: param1 + param2 };
};
store.registerAction("FakeAction", fakeAction as any);
store.pipe(fakeAction, "A", "B").dispatch().catch(() => { /**/ });
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.equal(state.foo, "AB");
done();
});
});
it("should queue the next state after dispatching an action", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = (currentState: testState) => {
return { ...currentState, ...modifiedState };
};
store.registerAction("FakeAction", fakeAction);
store.pipe(fakeAction).dispatch().catch(() => { /**/ });
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should accept the previously registered action name as pipe argument", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = async (_: testState) => Promise.resolve(modifiedState);
const fakeActionRegisteredName = "FakeAction";
store.registerAction(fakeActionRegisteredName, fakeAction);
store.pipe(fakeActionRegisteredName).dispatch().catch(() => { /**/ });
// since the async action is coming at a later time we need to skip the initial state
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should not accept an unregistered action name as pipe argument", function () {
const { store } = createTestStore();
const unregisteredActionId = "UnregisteredAction";
assert.throws(() => store.pipe(unregisteredActionId), UnregisteredActionError);
});
it("should support promised actions", function (done) {
const { store } = createTestStore();
const modifiedState = { foo: "bert" };
const fakeAction = async (_: testState) => Promise.resolve(modifiedState);
store.registerAction("FakeAction", fakeAction);
store.pipe(fakeAction).dispatch().catch(() => { /**/ });
// since the async action is coming at a later time we need to skip the initial state
store.state.pipe(
skip(1)
).subscribe((state) => {
assert.deepEqual(state, modifiedState);
done();
});
});
it("should dispatch actions one after another", function (done) {
const { store } = createTestStore();
const actionA = async (currentState: testState) => Promise.resolve({ foo: `${currentState.foo}A` });
const actionB = async (currentState: testState) => Promise.resolve({ foo: `${currentState.foo}B` });
store.registerAction("Action A", actionA);
store.registerAction("Action B", actionB);
store.pipe(actionA).dispatch().catch(() => { /**/ });
store.pipe(actionB).dispatch().catch(() => { /**/ });
store.state.pipe(
skip(2)
).subscribe((state) => {
assert.equal(state.foo, "barAB");
done();
});
});
it("should maintain queue of execution in concurrency constraints", function () {
const { store } = createTestStore();
createCallCounter((store as any).dispatchQueue, "push");
const { spyObj } = createCallCounter(store, "handleQueue");
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.pipe(actionA).dispatch().catch(() => { /**/ });
assert.equal(spyObj.callCounter, 0);
});
it("should log info about dispatched action if turned on via options", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, { logDispatchedActions: true });
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.pipe(actionA).dispatch().catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
});
it("should log info about dispatched action if turned on via options via custom loglevel", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, {
logDispatchedActions: true,
logDefinitions: {
dispatchedActions: LogLevel.debug
}
});
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.debug);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.pipe(actionA).dispatch().catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
});
it("should log info about dispatched action and return to default log level if wrong one provided", function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(initialState, {
logDispatchedActions: true,
logDefinitions: {
dispatchedActions: "foo" as any
}
});
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info);
const actionA = async (_: testState) => Promise.resolve({ foo: "A" });
store.registerAction("Action A", actionA);
store.pipe(actionA).dispatch().catch(() => { /**/ });
assert.equal(loggerSpy.callCounter, 1);
});
it("should log start-end dispatch duration if turned on via options", async function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(
initialState,
{ measurePerformance: PerformanceMeasurement.StartEnd }
);
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info, false);
const actionA = async (_: testState) => {
return new Promise<testState>((resolve) => {
setTimeout(() => resolve({ foo: "A" }), 1);
});
};
store.registerAction("Action A", actionA);
await store.pipe(actionA).dispatch();
assert.typeOf(loggerSpy.lastArgs[0], "string");
assert.equal(Array.isArray(loggerSpy.lastArgs[1]), true);
});
it("should log all dispatch durations if turned on via options", async function () {
const initialState: testState = {
foo: "bar"
};
const store = createStoreWithStateAndOptions<testState>(
initialState,
{ measurePerformance: PerformanceMeasurement.All }
);
const { spyObj: loggerSpy } = createCallCounter((store as any).logger, LogLevel.info, false);
const actionA = async (_: testState) => {
return new Promise<testState>((resolve) => {
setTimeout(() => resolve({ foo: "A" }), 1);
});
};
store.registerAction("Action A", actionA);
await store.pipe(actionA).dispatch();
assert.typeOf(loggerSpy.lastArgs[0], "string");
assert.equal(Array.isArray(loggerSpy.lastArgs[1]), true);
});
});
describe("internalDispatch", function () {
it("should throw an error when called with unregistered actions", function () {
const { store } = createTestStore();
const unregisteredAction = (currentState: testState, param1: string) => {
return { ...currentState, foo: param1 };
};
return assert.rejects(() => (store as any).internalDispatch([{ reducer: unregisteredAction, params: ["foo"] }]), UnregisteredActionError);
});
it("should throw an error when one action of multiple actions is unregistered", function () {
const { store } = createTestStore();
const registeredAction = (currentState: testState) => currentState;
const unregisteredAction = (currentState: testState) => currentState;
store.registerAction("RegisteredAction", registeredAction);
return assert.rejects(() => (store as any).internalDispatch([
{ reducer: registeredAction, params: [] },
{ reducer: unregisteredAction, params: [] }
]), UnregisteredActionError);
});
it("should throw an error about the first of many unregistered actions", function () {
const { store } = createTestStore();
const registeredAction = (currentState: testState) => currentState;
const firstUnregisteredAction = (currentState: testState) => currentState;
const secondUnregisteredAction = (currentState: testState) => currentState;
store.registerAction("RegisteredAction", registeredAction);
return assert.rejects((store as any).internalDispatch([
{ reducer: registeredAction, params: [] },
{ reducer: firstUnregisteredAction, params: [] },
{ reducer: secondUnregisteredAction, params: [] }
]), UnregisteredActionError);
});
});
});