UNPKG

@player-ui/player

Version:

382 lines (333 loc) 8.29 kB
import { describe, it, test, expect, vitest } from "vitest"; import { FlowInstance } from ".."; test("starts the right state", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "View2", }, }, } as const); flow.start(); expect(flow.currentState!.name).toBe("View1"); }); test("works with just END state", async () => { const flow = new FlowInstance("flow", { startState: "END_done", END_before_topic: { state_type: "END", outcome: "BACK" }, END_done: { state_type: "END", outcome: "doneWithTopic" }, } as const); expect(await flow.start()).toStrictEqual({ state_type: "END", outcome: "doneWithTopic", }); }); test("simple transitions between states", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "View2", }, }, View2: { state_type: "VIEW", ref: "foo", transitions: { Prev: "View1", }, }, } as const); flow.start(); expect(flow.currentState!.name).toBe("View1"); flow.transition("Next"); expect(flow.currentState!.name).toBe("View2"); flow.transition("Prev"); expect(flow.currentState!.name).toBe("View1"); }); test("transition from end state returns", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "Done1", }, }, Done1: { state_type: "END", outcome: "foo-bar", }, }); flow.start(); flow.transition("Next"); expect(() => flow.transition("Prev")).not.toThrowError(); }); test("fails when theres no startState", async () => { const flow = new FlowInstance("flow", { View1: { state_type: "Foo", transitions: {}, }, } as any); await expect(flow.start()).rejects.toThrowError( "No 'startState' defined for flow", ); }); test("uses * as fallback transition", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { "*": "SpecialView", }, }, SpecialView: { ref: "foo", state_type: "VIEW", transitions: {}, }, }); flow.start(); expect(flow.currentState!.name).toBe("View1"); flow.transition("Prev"); expect(flow.currentState!.name).toBe("SpecialView"); }); test("Do not throw exception when no transitions", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "View2", }, }, }); flow.start(); expect(flow.currentState!.name).toBe("View1"); expect(() => flow.transition("Prev")).not.toThrowError(); }); test("Fails to transition when not started", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "View2", }, }, }); expect(() => flow.transition("foo")).toThrowError(); }); test("Fails to transition during another transition", async () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", onStart: "foo bar", ref: "foo", transitions: { Next: "View2", }, }, View2: { state_type: "VIEW", ref: "bar", transitions: { Next: "View3", }, }, }); let deferredVar: string; const transition = () => { try { flow.transition("Next"); return "foo"; } catch (error: unknown) { return "bar"; } }; flow.hooks.resolveTransitionNode.intercept({ call: (nextState) => { if (nextState?.onStart) { deferredVar = transition(); } }, }); flow.start(); await vitest.waitFor(() => { expect(deferredVar).toBeDefined(); }); expect(deferredVar!).toBe("bar"); }); describe("promise api", () => { it("resolves when were done", async () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "Done1", }, }, Done1: { state_type: "END", outcome: "foo-bar", }, }); const flowProm = flow.start(); flow.transition("Next"); const result = await flowProm; expect(result.state_type).toBe("END"); expect(result.outcome).toBe("foo-bar"); }); }); test("reuses the same promise if started again", async () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { "*": "End1" }, }, End1: { state_type: "END", outcome: "bar", }, }); flow.start(); flow.transition("foo"); const result = await flow.start(); expect(result.outcome).toBe("bar"); }); test("calls onStart hook", async () => { const flow = new FlowInstance("flow", { onStart: "foo bar", } as any); const hook = vitest.fn(); flow.hooks.onStart.tap("test", hook); const result = flow.start(); expect(hook).toBeCalledWith("foo bar"); await expect(result).rejects.toThrowError("No 'startState' defined for flow"); }); test("calls the onEnd hook", async () => { const flow = new FlowInstance("flow", { onEnd: "foo bar", startState: "FOO", FOO: { state_type: "END", outcome: "done", }, }); const hook = vitest.fn(); flow.hooks.onEnd.tap("test", hook); const result = flow.start(); expect(hook).toBeCalledWith("foo bar"); expect(await result).toStrictEqual({ state_type: "END", outcome: "done", }); }); test("keeps current state if skipped", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "View2", }, }, View2: { state_type: "VIEW", ref: "bar", transitions: {}, }, }); flow.hooks.skipTransition.tap("test", (curr) => curr !== undefined); flow.start(); expect(flow.currentState!.name).toBe("View1"); flow.transition("Next"); expect(flow.currentState!.name).toBe("View1"); }); test("fails to transition if not started", () => { const flow = new FlowInstance("flow", { startState: "View1", }); expect(() => flow.transition("foo")).toThrowError(); }); test("fails if no transition", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", }, } as any); flow.start(); expect(() => flow.transition("foo")).toThrowError(); }); test("fails if no startState points to unknown state", async () => { const flow = new FlowInstance("flow", { startState: "View2", } as any); await expect(flow.start()).rejects.toThrowError( "No flow definition for: View2 was found.", ); }); test("force transition", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "Done1", }, }, Done1: { state_type: "END", outcome: "foo-bar", }, }); flow.start(); flow.hooks.skipTransition.tap("tst", () => true); expect(flow.currentState?.name).toBe("View1"); // Transition without the force flag flow.transition("Next"); // Should stay on the same page expect(flow.currentState?.name).toBe("View1"); flow.transition("Next", { force: true }); // Forced transition ignore the skip hook expect(flow.currentState?.name).toBe("Done1"); }); test("fails if transitioning to unknown state", () => { const flow = new FlowInstance("flow", { startState: "View1", View1: { state_type: "VIEW", ref: "foo", transitions: { Next: "Done1", }, }, Done1: { outcome: "foo-bar", }, } as any); flow.start(); expect(flow.currentState?.name).toBe("View1"); flow.transition("Next"); expect(flow.currentState?.name).toBe("View1"); });