soseki 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.config/_is-debug-mode.ts +9 -0
  2. package/.config/dependency-cruiser.cjs +408 -0
  3. package/.config/env.d.ts +8 -0
  4. package/.config/mise.toml +28 -0
  5. package/.config/navigation-api.d.ts +18 -0
  6. package/.config/tsconfig.build.json +28 -0
  7. package/.config/tsconfig.config.json +21 -0
  8. package/.config/tsconfig.test.json +26 -0
  9. package/.config/tsconfig.web.json +26 -0
  10. package/.config/vitest.client.ts +30 -0
  11. package/package.json +45 -0
  12. package/src/components/action-id.tsx +35 -0
  13. package/src/components/browser-router.tsx +31 -0
  14. package/src/components/hidden-input.tsx +39 -0
  15. package/src/components/outlet.tsx +17 -0
  16. package/src/components/router.tsx +234 -0
  17. package/src/contexts/route-context.ts +21 -0
  18. package/src/contexts/router-context.ts +55 -0
  19. package/src/core/_action-id-registry.ts +11 -0
  20. package/src/core/_capture-stack-trace.ts +12 -0
  21. package/src/core/_compare-route-paths.ts +90 -0
  22. package/src/core/_create-html-form-element-form-form-data.ts +32 -0
  23. package/src/core/_encode-pathname.ts +17 -0
  24. package/src/core/_is-error.ts +16 -0
  25. package/src/core/_is-promise-like.ts +14 -0
  26. package/src/core/_match-route-path.ts +39 -0
  27. package/src/core/_process-routes.ts +56 -0
  28. package/src/core/_singleton.ts +45 -0
  29. package/src/core/_unreachable.ts +22 -0
  30. package/src/core/_use-singleton.ts +24 -0
  31. package/src/core/_valibot.ts +147 -0
  32. package/src/core/_weak-id-registry.ts +125 -0
  33. package/src/core/constants.ts +4 -0
  34. package/src/core/data-map.types.ts +28 -0
  35. package/src/core/data-store.types.ts +25 -0
  36. package/src/core/deferred-promise.ts +408 -0
  37. package/src/core/errors.ts +680 -0
  38. package/src/core/expect-history-entry.ts +95 -0
  39. package/src/core/history-entry-id-schema.ts +27 -0
  40. package/src/core/history-entry-url-schema.ts +35 -0
  41. package/src/core/init-loaders.ts +79 -0
  42. package/src/core/match-routes.ts +91 -0
  43. package/src/core/readonly-form-data.types.ts +63 -0
  44. package/src/core/readonly-url.types.ts +156 -0
  45. package/src/core/redirect-response.ts +36 -0
  46. package/src/core/route-request.ts +92 -0
  47. package/src/core/route.types.ts +351 -0
  48. package/src/core/start-actions.ts +274 -0
  49. package/src/core/start-loaders.ts +254 -0
  50. package/src/core.ts +43 -0
  51. package/src/engines/engine.types.ts +216 -0
  52. package/src/engines/navigation-api-engine.ts +406 -0
  53. package/src/hooks/_use-route-context.ts +19 -0
  54. package/src/hooks/_use-router-context.ts +25 -0
  55. package/src/hooks/use-action-data.ts +37 -0
  56. package/src/hooks/use-loader-data.ts +28 -0
  57. package/src/hooks/use-navigate.ts +64 -0
  58. package/src/hooks/use-params.ts +11 -0
  59. package/src/hooks/use-pathname.ts +10 -0
  60. package/src/hooks/use-submit.ts +111 -0
  61. package/src/soseki.ts +75 -0
  62. package/src/utils/get-action-id.ts +12 -0
  63. package/src/utils/href.ts +17 -0
  64. package/src/utils/redirect.ts +13 -0
  65. package/src/utils/route-index.ts +70 -0
  66. package/src/utils/route-route.ts +111 -0
  67. package/src/utils/set-action-id.ts +14 -0
  68. package/tests/core/_capture-stack-trace.test.ts +46 -0
  69. package/tests/core/_compare-route-paths.test.ts +134 -0
  70. package/tests/core/_encode-pathname.test.ts +77 -0
  71. package/tests/core/_is-error.test.ts +108 -0
  72. package/tests/core/_is-promise-like.test.ts +100 -0
  73. package/tests/core/_match-route-path.test.ts +74 -0
  74. package/tests/core/_process-routes.test.ts +146 -0
  75. package/tests/core/_singleton.test.ts +102 -0
  76. package/tests/core/_unreachable.test.ts +38 -0
  77. package/tests/core/_use-singleton.test.ts +47 -0
  78. package/tests/core/_weak-id-registry.test.ts +137 -0
  79. package/tests/core/deferred-promise.test.ts +218 -0
  80. package/tests/core/expect-history-entry.test.ts +112 -0
  81. package/tests/core/init-loaders.test.ts +172 -0
  82. package/tests/core/match-routes.test.ts +178 -0
  83. package/tests/core/redirect-response.test.ts +36 -0
  84. package/tests/core/route-request.test.ts +93 -0
  85. package/tests/core/start-actions.test.ts +319 -0
  86. package/tests/core/start-loaders.test.ts +276 -0
  87. package/tests/engines/navigation-api-engine.test.ts +162 -0
  88. package/tsconfig.json +7 -0
@@ -0,0 +1,178 @@
1
+ import { test } from "vitest";
2
+ import matchRoutes from "../../src/core/match-routes.js";
3
+ import type { Route } from "../../src/core/route.types.js";
4
+
5
+ test("指定したパス名がどのルートにもマッチしないとき、null を返す", ({ expect }) => {
6
+ // Arrange
7
+ const routes: Route[] = [
8
+ {
9
+ path: "/home",
10
+ pathPattern: /^\/home\/?$/i,
11
+ paramKeys: [],
12
+ index: true,
13
+ dataFuncs: [],
14
+ component: () => null,
15
+ children: [],
16
+ },
17
+ ];
18
+
19
+ // Act
20
+ const result = matchRoutes(routes, "/about");
21
+
22
+ // Assert
23
+ expect(result).toBeNull();
24
+ });
25
+
26
+ test("単一のルートにマッチしたとき、そのルートの属性と抽出されたパラメーターを返す", ({ expect }) => {
27
+ // Arrange
28
+ const routes: Route[] = [
29
+ {
30
+ path: "/user/:id",
31
+ pathPattern: /^\/user\/([^/]+?)\/?$/i,
32
+ paramKeys: ["id"],
33
+ index: true,
34
+ dataFuncs: [],
35
+ component: () => null,
36
+ children: [],
37
+ },
38
+ ];
39
+
40
+ // Act
41
+ const result = matchRoutes(routes, "/user/123");
42
+
43
+ // Assert
44
+ expect(result).toHaveLength(1);
45
+ expect(result![0].path).toBe("/user/:id");
46
+ expect(result![0].index).toBe(true);
47
+ expect(result![0].params).toStrictEqual({ id: "123" });
48
+ expect(result![0].urlPath).toBe("/user/123");
49
+ });
50
+
51
+ test("パスパターンよりネストされたルートにマッチしたとき、urlPath はパスパターンまでの情報のみを保持する", ({ expect }) => {
52
+ // Arrange
53
+ const routes: Route[] = [
54
+ {
55
+ path: "/user/:id",
56
+ pathPattern: /^\/user\/([^/]+?)(?:\/.*)?$/i,
57
+ paramKeys: ["id"],
58
+ index: false,
59
+ dataFuncs: [],
60
+ component: () => null,
61
+ children: [],
62
+ },
63
+ ];
64
+
65
+ // Act
66
+ const result = matchRoutes(routes, "/user/123/setting");
67
+
68
+ // Assert
69
+ expect(result).toHaveLength(1);
70
+ expect(result![0].path).toBe("/user/:id");
71
+ expect(result![0].index).toBe(false);
72
+ expect(result![0].params).toStrictEqual({ id: "123" });
73
+ expect(result![0].urlPath).toBe("/user/123");
74
+ });
75
+
76
+ test("ネストされたルートにマッチしたとき、子から親への階層構造を保持した配列を返す", ({ expect }) => {
77
+ // Arrange
78
+ const childRoute: Route = {
79
+ path: "/parent/child",
80
+ pathPattern: /^\/parent\/child\/?$/i,
81
+ paramKeys: [],
82
+ index: true,
83
+ dataFuncs: [],
84
+ component: () => "Child",
85
+ children: [],
86
+ };
87
+
88
+ const parentRoute: Route = {
89
+ path: "/parent",
90
+ pathPattern: /^\/parent(?:\/.*)?$/i,
91
+ paramKeys: [],
92
+ index: false,
93
+ dataFuncs: [],
94
+ component: () => "Parent",
95
+ children: [childRoute],
96
+ };
97
+
98
+ const routes = [parentRoute];
99
+
100
+ // Act
101
+ const result = matchRoutes(routes, "/parent/child");
102
+
103
+ // Assert
104
+ expect(result).toHaveLength(2);
105
+ expect(result![0].path).toBe("/parent/child");
106
+ expect(result![0].index).toBe(true);
107
+ expect(result![0].urlPath).toBe("/parent/child");
108
+ expect(result![1]!.path).toBe("/parent");
109
+ expect(result![1]!.path).toBe("/parent");
110
+ expect(result![1]!.urlPath).toBe("/parent");
111
+ });
112
+
113
+ test("複数のルート定義があるとき、最初にマッチしたルートの結果を返す", ({ expect }) => {
114
+ // Arrange
115
+ const routes: Route[] = [
116
+ {
117
+ path: "/a",
118
+ pathPattern: /^\/a\/?$/i,
119
+ paramKeys: [],
120
+ index: true,
121
+ dataFuncs: [],
122
+ component: () => "A",
123
+ children: [],
124
+ },
125
+ {
126
+ path: "/:id",
127
+ pathPattern: /^\/([^/]+?)\/?$/i,
128
+ paramKeys: ["id"],
129
+ index: true,
130
+ dataFuncs: [],
131
+ component: () => "Dynamic",
132
+ children: [],
133
+ },
134
+ ];
135
+
136
+ // Act
137
+ const result1 = matchRoutes(routes, "/a");
138
+ const result2 = matchRoutes(routes.toReversed(), "/a");
139
+
140
+ // Assert
141
+ expect(result1).toHaveLength(1);
142
+ expect(result1![0].path).toBe("/a");
143
+ expect(result2).toHaveLength(1);
144
+ expect(result2![0].path).toBe("/:id");
145
+ });
146
+
147
+ test("親ルートはマッチするが子ルートのいずれにもマッチしないとき、親ルートのみを含む配列を返す", ({ expect }) => {
148
+ // Arrange
149
+ const childRoute: Route = {
150
+ path: "/parent/child",
151
+ pathPattern: /^\/parent\/child\/?$/i,
152
+ paramKeys: [],
153
+ index: true,
154
+ dataFuncs: [],
155
+ component: () => "Child",
156
+ children: [],
157
+ };
158
+
159
+ const parentRoute: Route = {
160
+ path: "/parent",
161
+ pathPattern: /^\/parent(?:\/.*)?$/i,
162
+ paramKeys: [],
163
+ index: false,
164
+ dataFuncs: [],
165
+ component: () => "Parent",
166
+ children: [childRoute],
167
+ };
168
+
169
+ const routes = [parentRoute];
170
+
171
+ // Act
172
+ const result = matchRoutes(routes, "/parent/other");
173
+
174
+ // Assert
175
+ expect(result).toHaveLength(1);
176
+ expect(result![0].path).toBe("/parent");
177
+ expect(result![0].index).toBe(false);
178
+ });
@@ -0,0 +1,36 @@
1
+ import { test } from "vitest";
2
+ import RedirectResponse from "../../src/core/redirect-response.js";
3
+
4
+ test("パス名を渡してインスタンス化したとき、エンコードされたパス名が保持される", ({ expect }) => {
5
+ // Arrange
6
+ const rawPathname = "/テスト/データ";
7
+ const expectedEncodedPathname = "/%E3%83%86%E3%82%B9%E3%83%88/%E3%83%87%E3%83%BC%E3%82%BF";
8
+
9
+ // Act
10
+ const response = new RedirectResponse(rawPathname);
11
+
12
+ // Assert
13
+ expect(response.pathname).toBe(expectedEncodedPathname);
14
+ });
15
+
16
+ test("すでにエンコード済みのパス名を渡したとき、二重にエンコードされずそのままのパス名が保持される", ({ expect }) => {
17
+ // Arrange
18
+ const encodedPathname = "/%E3%83%86%E3%82%B9%E3%83%88";
19
+
20
+ // Act
21
+ const response = new RedirectResponse(encodedPathname);
22
+
23
+ // Assert
24
+ expect(response.pathname).toBe(encodedPathname);
25
+ });
26
+
27
+ test("空のパス名を渡したとき、ルートパスになる", ({ expect }) => {
28
+ // Arrange
29
+ const emptyPathname = "";
30
+
31
+ // Act
32
+ const response = new RedirectResponse(emptyPathname);
33
+
34
+ // Assert
35
+ expect(response.pathname).toBe("/");
36
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, test } from "vitest";
2
+ import type { ReadonlyFormData } from "../../src/core/readonly-form-data.types.js";
3
+ import RouteRequest from "../../src/core/route-request.js";
4
+
5
+ describe("インスタンス化", () => {
6
+ test("指定した HTTP メソッド、URL、AbortSignal、FormData で初期化した場合、それぞれのプロパティーに正しく保持される", ({ expect }) => {
7
+ // Arrange
8
+ const method = "POST";
9
+ const url = new URL("https://example.com/test?b=2&a=1");
10
+ const { signal } = new AbortController();
11
+ const formData = new FormData() as unknown as ReadonlyFormData;
12
+
13
+ // Act
14
+ const actual = new RouteRequest(method, url, signal, formData);
15
+
16
+ // Assert
17
+ expect(actual.method).toBe(method);
18
+ expect(actual.url).toBe(url);
19
+ expect(actual.signal).toBe(signal);
20
+ expect(actual.formData).toBe(formData);
21
+ });
22
+
23
+ test("FormData を省略して初期化した場合、formData プロパティーは null になる", ({ expect }) => {
24
+ // Arrange
25
+ const method = "GET";
26
+ const url = new URL("https://example.com/");
27
+ const { signal } = new AbortController();
28
+
29
+ // Act
30
+ const actual = new RouteRequest(method, url, signal);
31
+
32
+ // Assert
33
+ expect(actual.formData).toBeNull();
34
+ });
35
+ });
36
+
37
+ describe("toRequest", () => {
38
+ test("GET メソッドで Request オブジェクトを生成したとき、body が空で正しいメソッドと URL が設定される", ({ expect }) => {
39
+ // Arrange
40
+ const method = "GET";
41
+ const href = "https://example.com/api";
42
+ const url = new URL(href);
43
+ const { signal } = new AbortController();
44
+ const routeRequest = new RouteRequest(method, url, signal);
45
+
46
+ // Act
47
+ const actual = routeRequest.toRequest();
48
+
49
+ // Assert
50
+ expect(actual.method).toBe("GET");
51
+ expect(actual.url).toBe(href);
52
+ expect(actual.body).toBeNull();
53
+ expect(actual.signal).toEqual(signal);
54
+ });
55
+
56
+ test("POST メソッドと FormData を指定して Request オブジェクトを生成したとき、body に FormData が設定される", async ({ expect }) => {
57
+ // Arrange
58
+ const method = "POST";
59
+ const url = new URL("https://example.com/api");
60
+ const { signal } = new AbortController();
61
+ const formData = new FormData();
62
+ formData.append("key", "value");
63
+ const routeRequest = new RouteRequest(
64
+ method,
65
+ url,
66
+ signal,
67
+ formData as unknown as ReadonlyFormData,
68
+ );
69
+
70
+ // Act
71
+ const actual = routeRequest.toRequest();
72
+
73
+ // Assert
74
+ expect(actual.method).toBe("POST");
75
+ const actualFormData = await actual.formData();
76
+ expect(actualFormData.get("key")).toBe("value");
77
+ });
78
+
79
+ test("AbortSignal を渡して Request オブジェクトを生成したとき、signal が正しく伝播される", ({ expect }) => {
80
+ // Arrange
81
+ const controller = new AbortController();
82
+ const url = new URL("https://example.com/");
83
+ const routeRequest = new RouteRequest("GET", url, controller.signal);
84
+
85
+ // Act
86
+ const actual = routeRequest.toRequest();
87
+
88
+ // Assert
89
+ expect(actual.signal.aborted).toBe(false);
90
+ controller.abort();
91
+ expect(actual.signal.aborted).toBe(true);
92
+ });
93
+ });
@@ -0,0 +1,319 @@
1
+ import { describe, test, vi } from "vitest";
2
+ import actionIdRegistry from "../../src/core/_action-id-registry.js";
3
+ import { IDataStore } from "../../src/core/data-store.types.js";
4
+ import {
5
+ ActionConditionError,
6
+ ActionExecutionError,
7
+ MultipleRedirectError,
8
+ } from "../../src/core/errors.js";
9
+ import { HistoryEntryId } from "../../src/core/history-entry-id-schema.js";
10
+ import RedirectResponse from "../../src/core/redirect-response.js";
11
+ import { IAction } from "../../src/core/route.types.js";
12
+ import startActions, { StartActionsParams } from "../../src/core/start-actions.js";
13
+
14
+ describe("アクションの実行判定", () => {
15
+ test("アクションを持つルートが存在しないとき、undefined を返す", ({ expect }) => {
16
+ // Arrange
17
+ const params = createParams({
18
+ routes: [
19
+ {
20
+ params: {},
21
+ urlPath: "/",
22
+ dataFuncs: [{
23
+ action: undefined,
24
+ loader: undefined,
25
+ shouldAction: () => true,
26
+ shouldReload: () => expect.unreachable(),
27
+ }],
28
+ },
29
+ ],
30
+ });
31
+
32
+ // Act
33
+ const wait = startActions(params);
34
+
35
+ // Assert
36
+ expect(wait).toBeUndefined();
37
+ });
38
+
39
+ test("shouldAction が true を返すとき、アクションが実行される", async ({ expect }) => {
40
+ // Arrange
41
+ const action = async () => "結果";
42
+ const params = createParams({
43
+ routes: [
44
+ {
45
+ params: {},
46
+ urlPath: "/",
47
+ dataFuncs: [{
48
+ action,
49
+ loader: undefined,
50
+ shouldAction: () => true,
51
+ shouldReload: () => expect.unreachable(),
52
+ }],
53
+ },
54
+ ],
55
+ });
56
+
57
+ // Act
58
+ const wait = startActions(params);
59
+ const result = await wait!();
60
+
61
+ // Assert
62
+ expect(result.resultMap.get(action)).toBe("結果");
63
+ });
64
+
65
+ test("shouldAction が false を返すとき、アクションは実行されない", async ({ expect }) => {
66
+ // Arrange
67
+ const action = vi.fn();
68
+ const params = createParams({
69
+ routes: [
70
+ {
71
+ params: {},
72
+ urlPath: "/",
73
+ dataFuncs: [{
74
+ action,
75
+ loader: undefined,
76
+ shouldAction: () => false,
77
+ shouldReload: () => expect.unreachable(),
78
+ }],
79
+ },
80
+ ],
81
+ });
82
+
83
+ // Act
84
+ const wait = startActions(params);
85
+ await wait?.();
86
+
87
+ // Assert
88
+ expect(action).not.toHaveBeenCalled();
89
+ });
90
+
91
+ test("フォームデータに _soseki_action_id が含まれるとき、ID が一致するアクションのみが実行される", async ({ expect }) => {
92
+ // Arrange
93
+ const action1 = async () => "result1";
94
+ const action2 = async () => "result2";
95
+ actionIdRegistry.set(action1);
96
+ const id2 = actionIdRegistry.set(action2);
97
+
98
+ const formData = new FormData();
99
+ formData.set("_soseki_action_id", id2);
100
+ const params = createParams({
101
+ formData: formData,
102
+ routes: [
103
+ {
104
+ params: {},
105
+ urlPath: "/",
106
+ dataFuncs: [
107
+ {
108
+ action: action1,
109
+ loader: undefined,
110
+ shouldAction: ({ defaultShouldAction }) => defaultShouldAction,
111
+ shouldReload: () => expect.unreachable(),
112
+ },
113
+ {
114
+ action: action2,
115
+ loader: undefined,
116
+ shouldAction: ({ defaultShouldAction }) => defaultShouldAction,
117
+ shouldReload: () => expect.unreachable(),
118
+ },
119
+ ],
120
+ },
121
+ ],
122
+ });
123
+
124
+ // Act
125
+ const wait = startActions(params);
126
+ const result = await wait!();
127
+
128
+ // Assert
129
+ expect(result.resultMap.size).toBe(1);
130
+ expect(result.resultMap.has(action2)).toBe(true);
131
+ expect(result.resultMap.get(action2)).toBe("result2");
132
+ });
133
+ });
134
+
135
+ describe("アクションの実行結果", () => {
136
+ test("アクションが RedirectResponse を返したとき、結果の redirect フィールドにパスが設定される", async ({ expect }) => {
137
+ // Arrange
138
+ const action = async () => new RedirectResponse("/new-path");
139
+ const params = createParams({
140
+ routes: [
141
+ {
142
+ params: {},
143
+ urlPath: "/",
144
+ dataFuncs: [{
145
+ action,
146
+ loader: undefined,
147
+ shouldAction: () => true,
148
+ shouldReload: () => expect.unreachable(),
149
+ }],
150
+ },
151
+ ],
152
+ });
153
+
154
+ // Act
155
+ const wait = startActions(params);
156
+ const result = await wait!();
157
+
158
+ // Assert
159
+ expect(result.redirect).toBe("/new-path");
160
+ expect(result.resultMap.get(action)).toBeUndefined();
161
+ });
162
+
163
+ test("複数のアクションが実行され、一方がリダイレクトを返したとき、正常に処理される", async ({ expect }) => {
164
+ // Arrange
165
+ const action1 = async () => "data";
166
+ const action2 = async () => new RedirectResponse("/path");
167
+ const params = createParams({
168
+ routes: [
169
+ {
170
+ params: {},
171
+ urlPath: "/",
172
+ dataFuncs: [
173
+ {
174
+ action: action1,
175
+ loader: undefined,
176
+ shouldAction: () => true,
177
+ shouldReload: () => expect.unreachable(),
178
+ },
179
+ {
180
+ action: action2,
181
+ loader: undefined,
182
+ shouldAction: () => true,
183
+ shouldReload: () => expect.unreachable(),
184
+ },
185
+ ],
186
+ },
187
+ ],
188
+ });
189
+
190
+ // Act
191
+ const wait = startActions(params);
192
+ const result = await wait!();
193
+
194
+ // Assert
195
+ expect(result.redirect).toBe("/path");
196
+ expect(result.resultMap.get(action1)).toBe("data");
197
+ });
198
+ });
199
+
200
+ describe("異常系", () => {
201
+ test("shouldAction が非同期処理(Promise)を返したとき、ActionConditionError を含む ActionExecutionError が発生する", async ({ expect }) => {
202
+ // Arrange
203
+ const action = async () => "data";
204
+ const params = createParams({
205
+ routes: [
206
+ {
207
+ params: {},
208
+ urlPath: "/",
209
+ dataFuncs: [{
210
+ action,
211
+ loader: undefined,
212
+ shouldAction: async () => true,
213
+ shouldReload: () => expect.unreachable(),
214
+ }],
215
+ },
216
+ ],
217
+ });
218
+
219
+ // Act
220
+ const wait = startActions(params);
221
+
222
+ // Assert
223
+ try {
224
+ await wait!();
225
+ expect.unreachable();
226
+ } catch (ex) {
227
+ expect(ex).toBeInstanceOf(ActionExecutionError);
228
+ expect((ex as ActionExecutionError).meta.errors).toStrictEqual([
229
+ {
230
+ action,
231
+ reason: expect.any(ActionConditionError),
232
+ },
233
+ ]);
234
+ }
235
+ // await expect(wait!()).rejects.toThrow(ActionConditionError);
236
+ });
237
+
238
+ test("アクションの実行中に例外が発生したとき、ActionExecutionError が発生する", async ({ expect }) => {
239
+ // Arrange
240
+ const error = new Error("失敗");
241
+ const action = async () => {
242
+ throw error;
243
+ };
244
+ const params = createParams({
245
+ routes: [
246
+ {
247
+ params: {},
248
+ urlPath: "/",
249
+ dataFuncs: [{
250
+ action,
251
+ loader: undefined,
252
+ shouldAction: () => true,
253
+ shouldReload: () => expect.unreachable(),
254
+ }],
255
+ },
256
+ ],
257
+ });
258
+
259
+ // Act
260
+ const wait = startActions(params);
261
+
262
+ // Assert
263
+ await expect(wait!()).rejects.toThrow(ActionExecutionError);
264
+ });
265
+
266
+ test("複数のアクションが RedirectResponse を返したとき、MultipleRedirectError が発生する", async ({ expect }) => {
267
+ // Arrange
268
+ const action1 = async () => new RedirectResponse("/path1");
269
+ const action2 = async () => new RedirectResponse("/path2");
270
+ const params = createParams({
271
+ routes: [
272
+ {
273
+ params: {},
274
+ urlPath: "/",
275
+ dataFuncs: [
276
+ {
277
+ action: action1,
278
+ loader: undefined,
279
+ shouldAction: () => true,
280
+ shouldReload: () => expect.unreachable(),
281
+ },
282
+ {
283
+ action: action2,
284
+ loader: undefined,
285
+ shouldAction: () => true,
286
+ shouldReload: () => expect.unreachable(),
287
+ },
288
+ ],
289
+ },
290
+ ],
291
+ });
292
+
293
+ // Act
294
+ const wait = startActions(params);
295
+
296
+ // Assert
297
+ await expect(wait!()).rejects.toThrow(MultipleRedirectError);
298
+ });
299
+ });
300
+
301
+ /**
302
+ * テスト用の StartActionsParams を作成する。
303
+ */
304
+ function createParams(overrides: Partial<StartActionsParams> = {}): StartActionsParams {
305
+ return {
306
+ routes: [],
307
+ entry: {
308
+ id: "67b82512-bd7e-414f-942f-df733bd53a6e" as HistoryEntryId,
309
+ url: new URL("http://localhost/"),
310
+ },
311
+ formData: new FormData(),
312
+ dataStore: {
313
+ get: () => undefined,
314
+ set: () => {},
315
+ } as unknown as IDataStore<IAction>,
316
+ signal: new AbortController().signal,
317
+ ...overrides,
318
+ };
319
+ }