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,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
+ });