Skip to content

Commit 9116a68

Browse files
committed
feat: add new test function getChainedUpdate for testing subsequent updates
1 parent 546bd50 commit 9116a68

File tree

4 files changed

+309
-0
lines changed

4 files changed

+309
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { cmd } from "../../cmd";
2+
import type { InitResult } from "../../Types";
3+
import type { UpdateMap } from "../Types";
4+
import { getChainedUpdateFn } from "./getChainedUpdate";
5+
6+
interface Model {
7+
value1: string;
8+
value2: string;
9+
value3: string;
10+
}
11+
12+
interface Props {}
13+
14+
type Message =
15+
| { name: "first" }
16+
| { name: "second" }
17+
| { name: "third" }
18+
| { name: "firstWithDefer" }
19+
| { name: "jumpToThird" }
20+
| { name: "somethingAsync" };
21+
22+
const Msg = {
23+
first: (): Message => ({ name: "first" }),
24+
second: (): Message => ({ name: "second" }),
25+
third: (): Message => ({ name: "third" }),
26+
firstWithDefer: (): Message => ({ name: "firstWithDefer" }),
27+
jumpToThird: (): Message => ({ name: "jumpToThird" }),
28+
somethingAsync: (): Message => ({ name: "somethingAsync" }),
29+
};
30+
31+
function init(): InitResult<Model, Message> {
32+
return [
33+
{
34+
value1: "",
35+
value2: "",
36+
value3: "",
37+
},
38+
];
39+
}
40+
41+
const update: UpdateMap<Props, Model, Message> = {
42+
first(_msg, model) {
43+
model.value1 = "1";
44+
45+
return [cmd.ofMsg(Msg.second())];
46+
},
47+
48+
second(_msg, model) {
49+
model.value2 = "2";
50+
51+
return [cmd.ofMsg(Msg.third())];
52+
},
53+
54+
third(_msg, model) {
55+
model.value3 = "3";
56+
57+
return [];
58+
},
59+
60+
firstWithDefer(_msg, model, _props, { defer }) {
61+
defer(cmd.ofMsg(Msg.second()));
62+
63+
model.value1 = "1";
64+
65+
return [];
66+
},
67+
68+
jumpToThird() {
69+
return [cmd.ofMsg(Msg.third())];
70+
},
71+
72+
somethingAsync(_msg, model) {
73+
model.value1 = "async";
74+
75+
return [cmd.ofSuccess(doSomething, Msg.third)];
76+
},
77+
};
78+
79+
async function doSomething(): Promise<void> {
80+
// does nothing
81+
}
82+
83+
describe("getChainedUpdate", () => {
84+
const chainedUpdate = getChainedUpdateFn(update);
85+
86+
it("executes all messages and returns the correct end state", async () => {
87+
// act
88+
const model = await chainedUpdate(Msg.first(), init()[0], {});
89+
90+
// assert
91+
expect(model).toStrictEqual<Model>({ value1: "1", value2: "2", value3: "3" });
92+
});
93+
94+
it("executes all messages and returns the correct end state including deferred values", async () => {
95+
// act
96+
const model = await chainedUpdate(Msg.firstWithDefer(), init()[0], {});
97+
98+
// assert
99+
expect(model).toStrictEqual<Model>({ value1: "1", value2: "2", value3: "3" });
100+
});
101+
102+
it("only returns the partial updated state", async () => {
103+
// arrange
104+
const initialModel: Model = { value1: "0", value2: "0", value3: "0" };
105+
106+
// act
107+
const model = await chainedUpdate(Msg.jumpToThird(), initialModel, {});
108+
109+
// assert
110+
expect(model).toStrictEqual<Partial<Model>>({ value3: "3" });
111+
});
112+
113+
it("works with async operations", async () => {
114+
// act
115+
const model = await chainedUpdate(Msg.somethingAsync(), init()[0], {});
116+
117+
// assert
118+
expect(model).toStrictEqual<Partial<Model>>({ value1: "async", value3: "3" });
119+
});
120+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Immutable } from "immer";
2+
import type { Message, Nullable } from "../../Types";
3+
import type { UpdateFunctionOptions, UpdateMap } from "../Types";
4+
import { getUpdateAndExecCmdFn } from "./getUpdateFn";
5+
6+
function getChainedUpdateFn<TProps, TModel, TMessage extends Message>(
7+
updateMap: UpdateMap<TProps, TModel, TMessage>,
8+
): (
9+
msg: TMessage,
10+
model: Immutable<TModel>,
11+
props: TProps,
12+
optionsTemplate?: Partial<UpdateFunctionOptions<TProps, TModel, TMessage>>,
13+
) => Promise<Partial<TModel>> {
14+
return async function chainedUpdateFn(msg, model, props, optionsTemplate): Promise<Partial<TModel>> {
15+
let messages: TMessage[] = [msg];
16+
let currentModel = model;
17+
let totalUpdatedModel: Partial<TModel> = {};
18+
19+
const updatedAndExecFn = getUpdateAndExecCmdFn(updateMap);
20+
21+
while (messages.length > 0) {
22+
const currentMessages: Nullable<TMessage>[] = [];
23+
24+
for (const nextMsg of messages) {
25+
// biome-ignore lint/nursery/noAwaitInLoop: We need to await each update sequentially
26+
const [updatedModel, newMessages] = await updatedAndExecFn(nextMsg, currentModel, props, optionsTemplate);
27+
28+
currentModel = { ...currentModel, ...updatedModel };
29+
totalUpdatedModel = { ...totalUpdatedModel, ...updatedModel };
30+
31+
currentMessages.push(...newMessages);
32+
}
33+
34+
messages = currentMessages.filter((message) => message != null);
35+
}
36+
37+
return totalUpdatedModel;
38+
};
39+
}
40+
41+
export { getChainedUpdateFn };
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { cmd } from "../cmd";
2+
import type { InitResult, UpdateMap } from "../Types";
3+
import { getChainedUpdateFn } from "./getChainedUpdate";
4+
5+
interface Model {
6+
value1: string;
7+
value2: string;
8+
value3: string;
9+
}
10+
11+
interface Props {}
12+
13+
type Message =
14+
| { name: "first" }
15+
| { name: "second" }
16+
| { name: "third" }
17+
| { name: "firstWithDefer" }
18+
| { name: "jumpToThird" }
19+
| { name: "somethingAsync" };
20+
21+
const Msg = {
22+
first: (): Message => ({ name: "first" }),
23+
second: (): Message => ({ name: "second" }),
24+
third: (): Message => ({ name: "third" }),
25+
firstWithDefer: (): Message => ({ name: "firstWithDefer" }),
26+
jumpToThird: (): Message => ({ name: "jumpToThird" }),
27+
somethingAsync: (): Message => ({ name: "somethingAsync" }),
28+
};
29+
30+
function init(): InitResult<Model, Message> {
31+
return [
32+
{
33+
value1: "",
34+
value2: "",
35+
value3: "",
36+
},
37+
];
38+
}
39+
40+
const update: UpdateMap<Props, Model, Message> = {
41+
first() {
42+
return [{ value1: "1" }, cmd.ofMsg(Msg.second())];
43+
},
44+
45+
second() {
46+
return [{ value2: "2" }, cmd.ofMsg(Msg.third())];
47+
},
48+
49+
third() {
50+
return [{ value3: "3" }];
51+
},
52+
53+
firstWithDefer(_msg, _model, _props, { defer }) {
54+
defer({ value1: "1" });
55+
56+
return [{}, cmd.ofMsg(Msg.second())];
57+
},
58+
59+
jumpToThird() {
60+
return [{}, cmd.ofMsg(Msg.third())];
61+
},
62+
63+
somethingAsync() {
64+
return [{ value1: "async" }, cmd.ofSuccess(doSomething, Msg.third)];
65+
},
66+
};
67+
68+
async function doSomething(): Promise<void> {
69+
// does nothing
70+
}
71+
72+
describe("getChainedUpdate", () => {
73+
const chainedUpdate = getChainedUpdateFn(update);
74+
75+
it("executes all messages and returns the correct end state", async () => {
76+
// act
77+
const model = await chainedUpdate(Msg.first(), init()[0], {});
78+
79+
// assert
80+
expect(model).toStrictEqual<Model>({ value1: "1", value2: "2", value3: "3" });
81+
});
82+
83+
it("executes all messages and returns the correct end state including deferred values", async () => {
84+
// act
85+
const model = await chainedUpdate(Msg.firstWithDefer(), init()[0], {});
86+
87+
// assert
88+
expect(model).toStrictEqual<Model>({ value1: "1", value2: "2", value3: "3" });
89+
});
90+
91+
it("only returns the partial updated state", async () => {
92+
// arrange
93+
const initialModel: Model = { value1: "0", value2: "0", value3: "0" };
94+
95+
// act
96+
const model = await chainedUpdate(Msg.jumpToThird(), initialModel, {});
97+
98+
// assert
99+
expect(model).toStrictEqual<Partial<Model>>({ value3: "3" });
100+
});
101+
102+
it("works with async operations", async () => {
103+
// act
104+
const model = await chainedUpdate(Msg.somethingAsync(), init()[0], {});
105+
106+
// assert
107+
expect(model).toStrictEqual<Partial<Model>>({ value1: "async", value3: "3" });
108+
});
109+
});

src/testing/getChainedUpdate.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Message, Nullable, UpdateFunctionOptions, UpdateMap } from "../Types";
2+
import { getUpdateAndExecCmdFn } from "./getUpdateFn";
3+
4+
function getChainedUpdateFn<TProps, TModel, TMessage extends Message>(
5+
updateMap: UpdateMap<TProps, TModel, TMessage>,
6+
): (
7+
msg: TMessage,
8+
model: TModel,
9+
props: TProps,
10+
optionsTemplate?: Partial<UpdateFunctionOptions<TProps, TModel, TMessage>>,
11+
) => Promise<Partial<TModel>> {
12+
return async function chainedUpdateFn(msg, model, props, optionsTemplate): Promise<Partial<TModel>> {
13+
let messages: TMessage[] = [msg];
14+
let currentModel = model;
15+
let totalUpdatedModel: Partial<TModel> = {};
16+
17+
const updatedAndExecFn = getUpdateAndExecCmdFn(updateMap);
18+
19+
while (messages.length > 0) {
20+
const currentMessages: Nullable<TMessage>[] = [];
21+
22+
for (const nextMsg of messages) {
23+
// biome-ignore lint/nursery/noAwaitInLoop: We need to await each update sequentially
24+
const [updatedModel, newMessages] = await updatedAndExecFn(nextMsg, currentModel, props, optionsTemplate);
25+
26+
currentModel = { ...currentModel, ...updatedModel };
27+
totalUpdatedModel = { ...totalUpdatedModel, ...updatedModel };
28+
29+
currentMessages.push(...newMessages);
30+
}
31+
32+
messages = currentMessages.filter((message) => message != null);
33+
}
34+
35+
return totalUpdatedModel;
36+
};
37+
}
38+
39+
export { getChainedUpdateFn };

0 commit comments

Comments
 (0)