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.
- package/.config/_is-debug-mode.ts +9 -0
- package/.config/dependency-cruiser.cjs +408 -0
- package/.config/env.d.ts +8 -0
- package/.config/mise.toml +28 -0
- package/.config/navigation-api.d.ts +18 -0
- package/.config/tsconfig.build.json +28 -0
- package/.config/tsconfig.config.json +21 -0
- package/.config/tsconfig.test.json +26 -0
- package/.config/tsconfig.web.json +26 -0
- package/.config/vitest.client.ts +30 -0
- package/package.json +45 -0
- package/src/components/action-id.tsx +35 -0
- package/src/components/browser-router.tsx +31 -0
- package/src/components/hidden-input.tsx +39 -0
- package/src/components/outlet.tsx +17 -0
- package/src/components/router.tsx +234 -0
- package/src/contexts/route-context.ts +21 -0
- package/src/contexts/router-context.ts +55 -0
- package/src/core/_action-id-registry.ts +11 -0
- package/src/core/_capture-stack-trace.ts +12 -0
- package/src/core/_compare-route-paths.ts +90 -0
- package/src/core/_create-html-form-element-form-form-data.ts +32 -0
- package/src/core/_encode-pathname.ts +17 -0
- package/src/core/_is-error.ts +16 -0
- package/src/core/_is-promise-like.ts +14 -0
- package/src/core/_match-route-path.ts +39 -0
- package/src/core/_process-routes.ts +56 -0
- package/src/core/_singleton.ts +45 -0
- package/src/core/_unreachable.ts +22 -0
- package/src/core/_use-singleton.ts +24 -0
- package/src/core/_valibot.ts +147 -0
- package/src/core/_weak-id-registry.ts +125 -0
- package/src/core/constants.ts +4 -0
- package/src/core/data-map.types.ts +28 -0
- package/src/core/data-store.types.ts +25 -0
- package/src/core/deferred-promise.ts +408 -0
- package/src/core/errors.ts +680 -0
- package/src/core/expect-history-entry.ts +95 -0
- package/src/core/history-entry-id-schema.ts +27 -0
- package/src/core/history-entry-url-schema.ts +35 -0
- package/src/core/init-loaders.ts +79 -0
- package/src/core/match-routes.ts +91 -0
- package/src/core/readonly-form-data.types.ts +63 -0
- package/src/core/readonly-url.types.ts +156 -0
- package/src/core/redirect-response.ts +36 -0
- package/src/core/route-request.ts +92 -0
- package/src/core/route.types.ts +351 -0
- package/src/core/start-actions.ts +274 -0
- package/src/core/start-loaders.ts +254 -0
- package/src/core.ts +43 -0
- package/src/engines/engine.types.ts +216 -0
- package/src/engines/navigation-api-engine.ts +406 -0
- package/src/hooks/_use-route-context.ts +19 -0
- package/src/hooks/_use-router-context.ts +25 -0
- package/src/hooks/use-action-data.ts +37 -0
- package/src/hooks/use-loader-data.ts +28 -0
- package/src/hooks/use-navigate.ts +64 -0
- package/src/hooks/use-params.ts +11 -0
- package/src/hooks/use-pathname.ts +10 -0
- package/src/hooks/use-submit.ts +111 -0
- package/src/soseki.ts +75 -0
- package/src/utils/get-action-id.ts +12 -0
- package/src/utils/href.ts +17 -0
- package/src/utils/redirect.ts +13 -0
- package/src/utils/route-index.ts +70 -0
- package/src/utils/route-route.ts +111 -0
- package/src/utils/set-action-id.ts +14 -0
- package/tests/core/_capture-stack-trace.test.ts +46 -0
- package/tests/core/_compare-route-paths.test.ts +134 -0
- package/tests/core/_encode-pathname.test.ts +77 -0
- package/tests/core/_is-error.test.ts +108 -0
- package/tests/core/_is-promise-like.test.ts +100 -0
- package/tests/core/_match-route-path.test.ts +74 -0
- package/tests/core/_process-routes.test.ts +146 -0
- package/tests/core/_singleton.test.ts +102 -0
- package/tests/core/_unreachable.test.ts +38 -0
- package/tests/core/_use-singleton.test.ts +47 -0
- package/tests/core/_weak-id-registry.test.ts +137 -0
- package/tests/core/deferred-promise.test.ts +218 -0
- package/tests/core/expect-history-entry.test.ts +112 -0
- package/tests/core/init-loaders.test.ts +172 -0
- package/tests/core/match-routes.test.ts +178 -0
- package/tests/core/redirect-response.test.ts +36 -0
- package/tests/core/route-request.test.ts +93 -0
- package/tests/core/start-actions.test.ts +319 -0
- package/tests/core/start-loaders.test.ts +276 -0
- package/tests/engines/navigation-api-engine.test.ts +162 -0
- 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
|
+
}
|