it-fsm/fsm.test.ts

232 lines
6.9 KiB
TypeScript
Raw Normal View History

2021-08-20 01:32:01 +03:00
import {
assertEquals,
assertThrows,
assertThrowsAsync,
} from "https://deno.land/std@0.105.0/testing/asserts.ts";
import * as fsm from "./fsm.ts";
enum ProjectStatus {
Pending = "pending",
Active = "active",
Completed = "completed",
Archived = "archive",
}
Deno.test("should add states separately in builder", function () {
const smb = new fsm.StateMachineBuilder()
.withState(ProjectStatus.Pending)
.withState(ProjectStatus.Active)
.withState(ProjectStatus.Completed)
.withState(ProjectStatus.Archived);
const states = smb[fsm._states];
assertEquals(states.size, 4);
assertEquals(Array.from(states.keys()), [
ProjectStatus.Pending,
ProjectStatus.Active,
ProjectStatus.Completed,
ProjectStatus.Archived,
]);
});
Deno.test("should bulk add states in builder", function () {
const stateNames = Object.values(ProjectStatus);
const smb = new fsm.StateMachineBuilder()
.withStates(stateNames);
const states = smb[fsm._states];
assertEquals(states.size, 4);
assertEquals(Array.from(states.keys()), stateNames);
});
Deno.test("should build without transitions", function () {
const sm = new fsm.StateMachineBuilder()
.withState(ProjectStatus.Pending)
.build(ProjectStatus.Pending);
assertEquals(sm.allowedTransitionStates(), []);
});
Deno.test("should build base state machine", function () {
const sm = new fsm.StateMachineBuilder()
.withStates(Object.values(ProjectStatus))
.withTransitions([
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
])
.build(ProjectStatus.Pending);
const [, active, , archived] = sm[fsm._states];
assertEquals(sm.allowedTransitionStates(), [active, archived]);
});
Deno.test("should change state", async function () {
const sm = new fsm.StateMachineBuilder()
.withStates(Object.values(ProjectStatus))
.withTransitions([
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
[ProjectStatus.Active, [ProjectStatus.Completed]],
])
.build(ProjectStatus.Pending);
const [, active, completed, archived] = sm[fsm._states];
assertEquals(sm.allowedTransitionStates(), [active, archived]);
await sm.tryChangeState(ProjectStatus.Active, null);
2021-08-20 01:32:01 +03:00
assertEquals(sm.allowedTransitionStates(), [completed]);
await sm.tryChangeState(ProjectStatus.Completed, null);
2021-08-20 01:32:01 +03:00
assertEquals(sm.allowedTransitionStates(), []);
});
Deno.test("should trigger state actions", async function () {
2021-08-20 11:11:06 +03:00
const triggeredTimes = {
beforeExit: 0,
onEntry: 0,
};
2021-08-20 01:32:01 +03:00
const sm = new fsm.StateMachineBuilder()
.withStates(
Object.values(ProjectStatus),
{
2021-08-20 11:11:06 +03:00
onEntry() {
triggeredTimes.onEntry += 1;
2021-08-20 01:32:01 +03:00
},
2021-08-20 11:11:06 +03:00
beforeExit() {
triggeredTimes.beforeExit += 1;
2021-08-20 01:32:01 +03:00
return true;
},
},
)
.withTransitions([
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
[ProjectStatus.Active, [ProjectStatus.Completed]],
])
.build(ProjectStatus.Pending);
const [, active, completed, archived] = sm[fsm._states];
2021-08-20 11:11:06 +03:00
assertEquals(triggeredTimes, { beforeExit: 0, onEntry: 0 });
2021-08-20 01:32:01 +03:00
assertEquals(sm.allowedTransitionStates(), [active, archived]);
assertEquals(
sm.allowedTransitionStateNames(),
[ProjectStatus.Active, ProjectStatus.Archived],
);
2021-08-20 01:32:01 +03:00
await sm.tryChangeState(ProjectStatus.Active, null);
2021-08-20 11:11:06 +03:00
assertEquals(triggeredTimes, { beforeExit: 1, onEntry: 1 });
2021-08-20 01:32:01 +03:00
assertEquals(sm.allowedTransitionStates(), [completed]);
assertEquals(sm.allowedTransitionStateNames(), [ProjectStatus.Completed]);
2021-08-20 01:32:01 +03:00
await sm.tryChangeState(ProjectStatus.Completed, null);
2021-08-20 11:11:06 +03:00
assertEquals(triggeredTimes, { beforeExit: 2, onEntry: 2 });
2021-08-20 01:32:01 +03:00
assertEquals(sm.allowedTransitionStates(), []);
assertEquals(sm.allowedTransitionStateNames(), []);
2021-08-20 01:32:01 +03:00
});
Deno.test("should stringify state", function () {
const pending = new fsm.State(ProjectStatus.Pending);
const active = new fsm.State(ProjectStatus.Active);
assertEquals(pending.toString(), ProjectStatus.Pending);
assertEquals(
[pending, active].join(),
`${ProjectStatus.Pending},${ProjectStatus.Active}`,
);
assertEquals(JSON.stringify({ pending }), '{"pending":"pending"}');
});
Deno.test("should throw type error if state doesn't exist", () => {
assertThrows(
() => new fsm.StateMachineBuilder().build(ProjectStatus.Pending),
TypeError,
"an instance of State class is expected",
);
});
Deno.test("should throw error if transition to the state doesn't exist", () => {
const sm = new fsm.StateMachineBuilder()
.withStates(Object.values(ProjectStatus))
.build(ProjectStatus.Pending);
assertThrowsAsync(
() => sm.tryChangeState(ProjectStatus.Active, null),
2021-08-20 01:32:01 +03:00
fsm.FsmError,
`cannot change state from "${ProjectStatus.Pending}" to "${ProjectStatus.Active}"`,
);
});
Deno.test("should return null if transition to the state doesn't exist", async () => {
const sm = new fsm.StateMachineBuilder()
.withStates(Object.values(ProjectStatus))
.build(ProjectStatus.Pending);
assertEquals(
await sm.maybeChangeState(ProjectStatus.Active, null),
null,
);
});
2021-08-20 01:32:01 +03:00
Deno.test("should throw error if beforeExit action returns false", () => {
const sm = new fsm.StateMachineBuilder()
.withStates(
Object.values(ProjectStatus),
{ beforeExit: () => false },
)
.withTransitions([
[ProjectStatus.Pending, [ProjectStatus.Active]],
])
.build(ProjectStatus.Pending);
assertThrowsAsync(
() => sm.tryChangeState(ProjectStatus.Active, null),
2021-08-20 01:32:01 +03:00
fsm.FsmError,
`cannot change state from "${ProjectStatus.Pending}" to "${ProjectStatus.Active}"`,
);
});
Deno.test("should reuse one builder for many entities", () => {
const statuses = Object.values(ProjectStatus);
const transitions: Array<[string, Array<string>]> = [
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
[ProjectStatus.Active, [ProjectStatus.Completed]],
];
const smb = new fsm.StateMachineBuilder()
.withStates(statuses)
.withTransitions(transitions);
function expectedAllowedStates(status: ProjectStatus) {
return transitions.find(([s]) => s === status)?.[1] || [];
}
for (const status of statuses) {
const sm = smb.build(status);
assertEquals(
sm.allowedTransitionStates().map(String),
expectedAllowedStates(status),
);
}
});
2021-08-23 11:12:08 +03:00
Deno.test("should trigger builded transition actions", async () => {
const states = ["locked", "unlocked"] as const;
const [locked, unlocked] = states;
const sm = new fsm.StateMachineBuilder()
.withStates([locked, unlocked])
.withTransitions([
[locked, { coin: unlocked }],
[unlocked, { push: locked }],
])
.build(locked);
const [lockedState, unlockedState] = sm[fsm._states];
assertEquals(await sm.trigger("coin", {}), unlockedState);
assertEquals(await sm.trigger("coin", {}), unlockedState);
assertEquals(await sm.trigger("push", {}), lockedState);
assertEquals(await sm.trigger("push", {}), lockedState);
});