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,137 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import WeakIdRegistry from "../../src/core/_weak-id-registry.js";
|
|
3
|
+
|
|
4
|
+
describe("オブジェクトの登録と ID の取得", () => {
|
|
5
|
+
test("未登録のオブジェクトを set すると、新しい ID が発行される", ({ expect }) => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const registry = new WeakIdRegistry<object>();
|
|
8
|
+
const obj = {};
|
|
9
|
+
|
|
10
|
+
// Act
|
|
11
|
+
const id = registry.set(obj);
|
|
12
|
+
|
|
13
|
+
// Assert
|
|
14
|
+
expect(typeof id).toBe("string");
|
|
15
|
+
expect(id.length).toBeGreaterThan(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("同じオブジェクトを再度 set すると、同じ ID が返される", ({ expect }) => {
|
|
19
|
+
// Arrange
|
|
20
|
+
const registry = new WeakIdRegistry<object>();
|
|
21
|
+
const obj = {};
|
|
22
|
+
const firstId = registry.set(obj);
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
const secondId = registry.set(obj);
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(secondId).toBe(firstId);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("異なるオブジェクトを set すると、それぞれ異なる ID が発行される", ({ expect }) => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const registry = new WeakIdRegistry<object>();
|
|
34
|
+
const obj1 = {};
|
|
35
|
+
const obj2 = {};
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const id1 = registry.set(obj1);
|
|
39
|
+
const id2 = registry.set(obj2);
|
|
40
|
+
|
|
41
|
+
// Assert
|
|
42
|
+
expect(id1).not.toBe(id2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("存在確認と双方向ルックアップ", () => {
|
|
47
|
+
test("登録済みのオブジェクトに対して has を呼び出すと、true が返る", ({ expect }) => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const registry = new WeakIdRegistry<object>();
|
|
50
|
+
const obj = {};
|
|
51
|
+
registry.set(obj);
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = registry.has(obj);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(result).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("未登録のオブジェクトに対して has を呼び出すと、false が返る", ({ expect }) => {
|
|
61
|
+
// Arrange
|
|
62
|
+
const registry = new WeakIdRegistry<object>();
|
|
63
|
+
const obj = {};
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
const result = registry.has(obj);
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
expect(result).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("オブジェクトから ID を取得すると、登録時に発行された ID と一致する", ({ expect }) => {
|
|
73
|
+
// Arrange
|
|
74
|
+
const registry = new WeakIdRegistry<object>();
|
|
75
|
+
const obj = {};
|
|
76
|
+
const expectedId = registry.set(obj);
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const actualId = registry.get(obj);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(actualId).toBe(expectedId);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("ID からオブジェクトを取得すると、元のオブジェクトが返る", ({ expect }) => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const registry = new WeakIdRegistry<object>();
|
|
88
|
+
const obj = {};
|
|
89
|
+
const id = registry.set(obj);
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
const retrievedObj = registry.get(id);
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(retrievedObj).toBe(obj);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("未登録のオブジェクトを get すると、undefined が返る", ({ expect }) => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const registry = new WeakIdRegistry<object>();
|
|
101
|
+
const obj = {};
|
|
102
|
+
|
|
103
|
+
// Act
|
|
104
|
+
const result = registry.get(obj);
|
|
105
|
+
|
|
106
|
+
// Assert
|
|
107
|
+
expect(result).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("存在しない ID を get すると、undefined が返る", ({ expect }) => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const registry = new WeakIdRegistry<object>();
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
const result = registry.get("non-existent-id");
|
|
116
|
+
|
|
117
|
+
// Assert
|
|
118
|
+
expect(result).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("ID 発行の仕様", () => {
|
|
123
|
+
test("複数のオブジェクトを登録したとき、ID は 36 進数のインクリメント形式で発行される", ({ expect }) => {
|
|
124
|
+
// Arrange
|
|
125
|
+
const registry = new WeakIdRegistry<object>();
|
|
126
|
+
const objects = [{}, {}, {}];
|
|
127
|
+
|
|
128
|
+
// Act
|
|
129
|
+
const ids = objects.map((obj) => registry.set(obj));
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(ids.length).toBe(3);
|
|
133
|
+
expect(ids[0]).toBe("0");
|
|
134
|
+
expect(ids[1]).toBe("1");
|
|
135
|
+
expect(ids[2]).toBe("2");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import DeferredPromise from "../../src/core/deferred-promise.js";
|
|
3
|
+
|
|
4
|
+
describe("インスタンス化と基本状態", () => {
|
|
5
|
+
test("新規作成されたとき、ステータスは pending になる", ({ expect }) => {
|
|
6
|
+
// Arrange & Act
|
|
7
|
+
const { promise } = DeferredPromise.withResolvers<string>();
|
|
8
|
+
|
|
9
|
+
// Assert
|
|
10
|
+
expect(promise.status).toBe("pending");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("resolve による解決", () => {
|
|
15
|
+
test("値を指定して resolve すると、ステータスが fulfilled になり、その値が保持される", ({ expect }) => {
|
|
16
|
+
// Arrange
|
|
17
|
+
const { promise, resolve } = DeferredPromise.withResolvers<string>();
|
|
18
|
+
const expectedValue = "成功";
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
resolve(expectedValue);
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(promise.status).toBe("fulfilled");
|
|
25
|
+
if (promise.status === "fulfilled") {
|
|
26
|
+
expect(promise.value).toBe(expectedValue);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("PromiseLike な値を resolve すると、その Promise の解決を待ってから自身のステータスを更新する", async ({ expect }) => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const { promise, resolve } = DeferredPromise.withResolvers<string>();
|
|
33
|
+
const innerPromise = Promise.resolve("内部解決");
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
resolve(innerPromise);
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
// resolve 直後は pending(マイクロタスクで処理されるため)
|
|
40
|
+
expect(promise.status).toBe("pending");
|
|
41
|
+
|
|
42
|
+
// Promise の解決を待機
|
|
43
|
+
await innerPromise;
|
|
44
|
+
// 内部の then が呼ばれるのを待つために少し待機
|
|
45
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
46
|
+
|
|
47
|
+
expect(promise.status).toBe("fulfilled");
|
|
48
|
+
if (promise.status === "fulfilled") {
|
|
49
|
+
expect(promise.value).toBe("内部解決");
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("reject による拒否", () => {
|
|
55
|
+
test("理由を指定して reject すると、ステータスが rejected になり、その理由が保持される", ({ expect }) => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const { promise, reject } = DeferredPromise.withResolvers<string>();
|
|
58
|
+
const reason = new Error("失敗");
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
reject(reason);
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(promise.status).toBe("rejected");
|
|
65
|
+
if (promise.status === "rejected") {
|
|
66
|
+
expect(promise.reason).toBe(reason);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("静的メソッド", () => {
|
|
72
|
+
test("DeferredPromise.resolve を使用したとき、即座に解決状態のインスタンスが生成される", ({ expect }) => {
|
|
73
|
+
// Arrange & Act
|
|
74
|
+
const value = 100;
|
|
75
|
+
const promise = DeferredPromise.resolve(value);
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(promise.status).toBe("fulfilled");
|
|
79
|
+
expect(promise.value).toBe(value);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("DeferredPromise.reject を使用したとき、即座に拒否状態のインスタンスが生成される", ({ expect }) => {
|
|
83
|
+
// Arrange & Act
|
|
84
|
+
const reason = "error";
|
|
85
|
+
const promise = DeferredPromise.reject(reason);
|
|
86
|
+
|
|
87
|
+
// Assert
|
|
88
|
+
expect(promise.status).toBe("rejected");
|
|
89
|
+
expect(promise.reason).toBe(reason);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("DeferredPromise.try で同期関数を実行したとき、その戻り値で解決される", ({ expect }) => {
|
|
93
|
+
// Arrange & Act
|
|
94
|
+
const promise = DeferredPromise.try(() => "sync result");
|
|
95
|
+
|
|
96
|
+
// Assert
|
|
97
|
+
expect(promise.status).toBe("fulfilled");
|
|
98
|
+
if (promise.status === "fulfilled") {
|
|
99
|
+
expect(promise.value).toBe("sync result");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("DeferredPromise.try で例外が発生したとき、その例外を理由として拒否される", ({ expect }) => {
|
|
104
|
+
// Arrange & Act
|
|
105
|
+
const error = new Error("throw error");
|
|
106
|
+
const promise = DeferredPromise.try(() => {
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
expect(promise.status).toBe("rejected");
|
|
112
|
+
if (promise.status === "rejected") {
|
|
113
|
+
expect(promise.reason).toBe(error);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("メソッドチェーン (then)", () => {
|
|
119
|
+
test("解決後に then で登録されたコールバックが実行され、新しい値で解決される", async ({ expect }) => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const { promise, resolve } = DeferredPromise.withResolvers<number>();
|
|
122
|
+
const results: number[] = [];
|
|
123
|
+
|
|
124
|
+
// Act
|
|
125
|
+
promise.then((val) => {
|
|
126
|
+
const next = val * 2;
|
|
127
|
+
results.push(next);
|
|
128
|
+
return next;
|
|
129
|
+
});
|
|
130
|
+
resolve(10);
|
|
131
|
+
|
|
132
|
+
// Assert
|
|
133
|
+
// 非同期実行を待機
|
|
134
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
135
|
+
|
|
136
|
+
expect(results.length).toBe(1);
|
|
137
|
+
expect(results[0]!).toBe(20);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("拒否後に then の第二引数(onRejected)が実行され、その戻り値で次の Promise が解決される", async ({ expect }) => {
|
|
141
|
+
// Arrange
|
|
142
|
+
const { promise, reject } = DeferredPromise.withResolvers<number>();
|
|
143
|
+
const error = new Error("fail");
|
|
144
|
+
let capturedReason: unknown;
|
|
145
|
+
|
|
146
|
+
// Act
|
|
147
|
+
const nextPromise = promise.then(
|
|
148
|
+
null,
|
|
149
|
+
(reason) => {
|
|
150
|
+
capturedReason = reason;
|
|
151
|
+
return "recovered";
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
reject(error);
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
158
|
+
|
|
159
|
+
expect(capturedReason).toBe(error);
|
|
160
|
+
// エラーハンドリングされると、後続は fulfilled になる仕様
|
|
161
|
+
expect(nextPromise.status).toBe("fulfilled");
|
|
162
|
+
if (nextPromise.status === "fulfilled") {
|
|
163
|
+
expect(nextPromise.value).toBe("recovered");
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("コールバック内で例外が発生したとき、後続の Promise が拒否される", async ({ expect }) => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const { promise, resolve } = DeferredPromise.withResolvers<string>();
|
|
170
|
+
const error = new Error("callback error");
|
|
171
|
+
|
|
172
|
+
// Act
|
|
173
|
+
const nextPromise = promise.then(() => {
|
|
174
|
+
throw error;
|
|
175
|
+
});
|
|
176
|
+
resolve("ok");
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
await new Promise<void>((r) => queueMicrotask(r));
|
|
180
|
+
|
|
181
|
+
expect(nextPromise.status).toBe("rejected");
|
|
182
|
+
if (nextPromise.status === "rejected") {
|
|
183
|
+
expect(nextPromise.reason).toBe(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("不変性", () => {
|
|
189
|
+
test("一度 fulfilled になった後は、再度 resolve しても値が変化しない", ({ expect }) => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const { promise, resolve } = DeferredPromise.withResolvers<string>();
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
resolve("first");
|
|
195
|
+
resolve("second");
|
|
196
|
+
|
|
197
|
+
// Assert
|
|
198
|
+
expect(promise.status).toBe("fulfilled");
|
|
199
|
+
if (promise.status === "fulfilled") {
|
|
200
|
+
expect(promise.value).toBe("first");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("一度 rejected になった後は、resolve してもステータスが変化しない", ({ expect }) => {
|
|
205
|
+
// Arrange
|
|
206
|
+
const { promise, resolve, reject } = DeferredPromise.withResolvers<string>();
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
reject("error");
|
|
210
|
+
resolve("success");
|
|
211
|
+
|
|
212
|
+
// Assert
|
|
213
|
+
expect(promise.status).toBe("rejected");
|
|
214
|
+
if (promise.status === "rejected") {
|
|
215
|
+
expect(promise.reason).toBe("error");
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import { UnexpectedValidationError } from "../../src/core/errors.js";
|
|
3
|
+
import expectHistoryEntry, { type HistoryEntryLike } from "../../src/core/expect-history-entry.js";
|
|
4
|
+
|
|
5
|
+
describe("有効な入力が与えられた場合", () => {
|
|
6
|
+
test("すべての項目が正当なとき、バリデーション済みの履歴エントリーを返す", ({ expect }) => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const input: HistoryEntryLike = {
|
|
9
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
10
|
+
url: "https://example.com/",
|
|
11
|
+
index: 0,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Act
|
|
15
|
+
const result = expectHistoryEntry(input);
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(result).not.toBeNull();
|
|
19
|
+
expect(result?.id).toBe(input.id);
|
|
20
|
+
expect(result?.url.href).toBe(input.url);
|
|
21
|
+
expect(result?.index).toBe(input.index);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("入力値が空または欠損している場合", () => {
|
|
26
|
+
test("null が与えられたとき、null を返す", ({ expect }) => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const input = null;
|
|
29
|
+
|
|
30
|
+
// Act
|
|
31
|
+
const result = expectHistoryEntry(input);
|
|
32
|
+
|
|
33
|
+
// Assert
|
|
34
|
+
expect(result).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("undefined が与えられたとき、null を返す", ({ expect }) => {
|
|
38
|
+
// Arrange
|
|
39
|
+
const input = undefined;
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
const result = expectHistoryEntry(input);
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("url が null のとき、null を返す", ({ expect }) => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const input: HistoryEntryLike = {
|
|
51
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
52
|
+
url: null,
|
|
53
|
+
index: 1,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Act
|
|
57
|
+
const result = expectHistoryEntry(input);
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("不正な形式のデータが与えられた場合", () => {
|
|
65
|
+
test("index が負の整数のとき、エラーを投げる", ({ expect }) => {
|
|
66
|
+
// Arrange
|
|
67
|
+
const input: HistoryEntryLike = {
|
|
68
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
69
|
+
url: "https://example.com/",
|
|
70
|
+
index: -1,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Act & Assert
|
|
74
|
+
expect(() => expectHistoryEntry(input)).toThrow(UnexpectedValidationError);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("id が UUID 形式でないとき、エラーを投げる", ({ expect }) => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const input: HistoryEntryLike = {
|
|
80
|
+
id: "invalid-id",
|
|
81
|
+
url: "https://example.com/",
|
|
82
|
+
index: 0,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Act & Assert
|
|
86
|
+
expect(() => expectHistoryEntry(input)).toThrow(UnexpectedValidationError);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("url が不正形式のとき、エラーを投げる", ({ expect }) => {
|
|
90
|
+
// Arrange
|
|
91
|
+
const input: HistoryEntryLike = {
|
|
92
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
93
|
+
url: "invalid-url",
|
|
94
|
+
index: 0,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Act & Assert
|
|
98
|
+
expect(() => expectHistoryEntry(input)).toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("index が整数ではないとき、エラーを投げる", ({ expect }) => {
|
|
102
|
+
// Arrange
|
|
103
|
+
const input: HistoryEntryLike = {
|
|
104
|
+
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
105
|
+
url: "https://example.com/",
|
|
106
|
+
index: 1.5,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Act & Assert
|
|
110
|
+
expect(() => expectHistoryEntry(input)).toThrow(UnexpectedValidationError);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { test, vi } from "vitest";
|
|
2
|
+
import type { HistoryEntryId } from "../../src/core/history-entry-id-schema.js";
|
|
3
|
+
import initLoaders, { type InitLoadersParams } from "../../src/core/init-loaders.js";
|
|
4
|
+
import RouteRequest from "../../src/core/route-request.js";
|
|
5
|
+
|
|
6
|
+
test("指定されたルートのローダーがすべて実行され、データストアに保存されるとき、すべての処理が開始されるまで待機する", async ({ expect }) => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const entryId = "59b2ba6e-5236-406c-9496-0a6bcd83b1ab" as HistoryEntryId;
|
|
9
|
+
const entryUrl = new URL("https://example.com/test");
|
|
10
|
+
const abortController = new AbortController();
|
|
11
|
+
|
|
12
|
+
const loader1 = vi.fn().mockResolvedValue("data 1");
|
|
13
|
+
const loader2 = vi.fn().mockResolvedValue("data 2");
|
|
14
|
+
|
|
15
|
+
const routes = [
|
|
16
|
+
{
|
|
17
|
+
params: { id: "1" },
|
|
18
|
+
dataFuncs: [{
|
|
19
|
+
action: undefined,
|
|
20
|
+
loader: loader1,
|
|
21
|
+
shouldAction: () => true,
|
|
22
|
+
shouldReload: () => true,
|
|
23
|
+
}],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
params: { id: "2" },
|
|
27
|
+
dataFuncs: [{
|
|
28
|
+
action: undefined,
|
|
29
|
+
loader: loader2,
|
|
30
|
+
shouldAction: () => true,
|
|
31
|
+
shouldReload: () => true,
|
|
32
|
+
}],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const dataStoreSetMock = vi.fn();
|
|
37
|
+
const dataStore = {
|
|
38
|
+
set: dataStoreSetMock,
|
|
39
|
+
} as any;
|
|
40
|
+
|
|
41
|
+
const params: InitLoadersParams = {
|
|
42
|
+
routes,
|
|
43
|
+
entry: { id: entryId, url: entryUrl },
|
|
44
|
+
dataStore,
|
|
45
|
+
signal: abortController.signal,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
await initLoaders(params);
|
|
50
|
+
|
|
51
|
+
// Assert
|
|
52
|
+
// データストアへの保存を検証
|
|
53
|
+
expect(dataStoreSetMock).toHaveBeenCalledTimes(1);
|
|
54
|
+
const [savedId, dataMap] = dataStoreSetMock.mock.calls[0]!;
|
|
55
|
+
expect(savedId).toBe(entryId);
|
|
56
|
+
expect(dataMap.size).toBe(2);
|
|
57
|
+
|
|
58
|
+
// 各ローダーに正しい引数が渡されていることを検証
|
|
59
|
+
expect(loader1).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
params: { id: "1" },
|
|
62
|
+
request: expect.any(RouteRequest),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(loader2).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
params: { id: "2" },
|
|
68
|
+
request: expect.any(RouteRequest),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("ローダーが定義されていないルートが含まれるとき、そのルートをスキップして処理を継続する", async ({ expect }) => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const entryId = "59b2ba6e-5236-406c-9496-0a6bcd83b1ab" as HistoryEntryId;
|
|
76
|
+
const entryUrl = new URL("https://example.com/mixed");
|
|
77
|
+
const loader = vi.fn().mockResolvedValue("data");
|
|
78
|
+
|
|
79
|
+
const routes = [
|
|
80
|
+
{
|
|
81
|
+
params: {},
|
|
82
|
+
dataFuncs: [{
|
|
83
|
+
action: undefined,
|
|
84
|
+
loader: undefined, // ローダーなし
|
|
85
|
+
shouldAction: () => true,
|
|
86
|
+
shouldReload: () => true,
|
|
87
|
+
}],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
params: { id: "exists" },
|
|
91
|
+
dataFuncs: [{
|
|
92
|
+
action: undefined,
|
|
93
|
+
loader,
|
|
94
|
+
shouldAction: () => true,
|
|
95
|
+
shouldReload: () => true,
|
|
96
|
+
}],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const dataStoreSetMock = vi.fn();
|
|
101
|
+
const dataStore = {
|
|
102
|
+
set: dataStoreSetMock,
|
|
103
|
+
} as any;
|
|
104
|
+
|
|
105
|
+
const params: InitLoadersParams = {
|
|
106
|
+
routes,
|
|
107
|
+
entry: { id: entryId, url: entryUrl },
|
|
108
|
+
dataStore,
|
|
109
|
+
signal: new AbortController().signal,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Act
|
|
113
|
+
await initLoaders(params);
|
|
114
|
+
|
|
115
|
+
// Assert
|
|
116
|
+
const [, dataMap] = dataStoreSetMock.mock.calls[0]!;
|
|
117
|
+
expect(dataMap.size).toBe(1);
|
|
118
|
+
expect(loader).toHaveBeenCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("ローダーがエラーを返したときでも、他のローダーの処理を妨げず、すべての待機を完了する", async ({ expect }) => {
|
|
122
|
+
// Arrange
|
|
123
|
+
const entryId = "59b2ba6e-5236-406c-9496-0a6bcd83b1ab" as HistoryEntryId;
|
|
124
|
+
const entryUrl = new URL("https://example.com/error");
|
|
125
|
+
|
|
126
|
+
// 成功するローダーと失敗するローダーを用意
|
|
127
|
+
const successLoader = vi.fn().mockResolvedValue("success");
|
|
128
|
+
const errorLoader = vi.fn().mockRejectedValue(new Error("Loader Failed"));
|
|
129
|
+
|
|
130
|
+
const routes = [
|
|
131
|
+
{
|
|
132
|
+
params: {},
|
|
133
|
+
dataFuncs: [{
|
|
134
|
+
action: undefined,
|
|
135
|
+
loader: errorLoader,
|
|
136
|
+
shouldAction: () => true,
|
|
137
|
+
shouldReload: () => true,
|
|
138
|
+
}],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
params: {},
|
|
142
|
+
dataFuncs: [{
|
|
143
|
+
action: undefined,
|
|
144
|
+
loader: successLoader,
|
|
145
|
+
shouldAction: () => true,
|
|
146
|
+
shouldReload: () => true,
|
|
147
|
+
}],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const dataStoreSetMock = vi.fn();
|
|
152
|
+
const dataStore = {
|
|
153
|
+
set: dataStoreSetMock,
|
|
154
|
+
} as any;
|
|
155
|
+
|
|
156
|
+
const params: InitLoadersParams = {
|
|
157
|
+
routes,
|
|
158
|
+
entry: { id: entryId, url: entryUrl },
|
|
159
|
+
dataStore,
|
|
160
|
+
signal: new AbortController().signal,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Act & Assert
|
|
164
|
+
// Promise.allSettled を使用しているため、個別のエラーで initLoaders 自体は reject されない
|
|
165
|
+
await expect(initLoaders(params)()).resolves.not.toThrow();
|
|
166
|
+
|
|
167
|
+
expect(successLoader).toHaveBeenCalled();
|
|
168
|
+
expect(errorLoader).toHaveBeenCalled();
|
|
169
|
+
|
|
170
|
+
const [, dataMap] = dataStoreSetMock.mock.calls[0]!;
|
|
171
|
+
expect(dataMap.size).toBe(2);
|
|
172
|
+
});
|