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,276 @@
|
|
|
1
|
+
import { describe, test, vi } from "vitest";
|
|
2
|
+
import { IDataStore } from "../../src/core/data-store.types.js";
|
|
3
|
+
import DeferredPromise from "../../src/core/deferred-promise.js";
|
|
4
|
+
import { LoaderConditionError } from "../../src/core/errors.js";
|
|
5
|
+
import { HistoryEntryId } from "../../src/core/history-entry-id-schema.js";
|
|
6
|
+
import { IAction, ILoader } from "../../src/core/route.types.js";
|
|
7
|
+
import startLoaders, { StartLoadersParams } from "../../src/core/start-loaders.js";
|
|
8
|
+
|
|
9
|
+
describe("ローダーの実行判定", () => {
|
|
10
|
+
test("前回のエントリーにデータが存在しないとき、ローダーが新規に実行される", async ({ expect }) => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const loader = vi.fn().mockResolvedValue("data");
|
|
13
|
+
const params = createParams({
|
|
14
|
+
currentRoutes: [
|
|
15
|
+
{
|
|
16
|
+
path: "/",
|
|
17
|
+
params: {},
|
|
18
|
+
dataFuncs: [{
|
|
19
|
+
action: undefined,
|
|
20
|
+
loader,
|
|
21
|
+
shouldAction: () => expect.unreachable(),
|
|
22
|
+
shouldReload: () => true,
|
|
23
|
+
}],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Act
|
|
29
|
+
const wait = startLoaders(params);
|
|
30
|
+
await wait?.();
|
|
31
|
+
|
|
32
|
+
// Assert
|
|
33
|
+
expect(loader).toHaveBeenCalledOnce();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("shouldReload が false を返すとき、前回のデータが引き継がれローダーは再実行されない", async ({ expect }) => {
|
|
37
|
+
// Arrange
|
|
38
|
+
const loader = vi.fn().mockResolvedValue("new data");
|
|
39
|
+
const prevData = Promise.resolve("old data");
|
|
40
|
+
|
|
41
|
+
const loaderDataStore = createStubDataStore();
|
|
42
|
+
// 前回のデータをストアにセットしておく。
|
|
43
|
+
const prevEntryId = "8ce9bfcd-fe55-440a-80db-e90cb6e93ac5" as HistoryEntryId;
|
|
44
|
+
const prevMap = new Map();
|
|
45
|
+
prevMap.set(loader, prevData);
|
|
46
|
+
loaderDataStore.set(prevEntryId, prevMap);
|
|
47
|
+
|
|
48
|
+
const params = createParams({
|
|
49
|
+
prevEntry: {
|
|
50
|
+
id: prevEntryId,
|
|
51
|
+
url: new URL("http://localhost/"),
|
|
52
|
+
},
|
|
53
|
+
loaderDataStore,
|
|
54
|
+
currentRoutes: [
|
|
55
|
+
{
|
|
56
|
+
path: "/",
|
|
57
|
+
params: {},
|
|
58
|
+
dataFuncs: [{
|
|
59
|
+
action: undefined,
|
|
60
|
+
loader,
|
|
61
|
+
shouldAction: () => expect.unreachable(),
|
|
62
|
+
shouldReload: () => false,
|
|
63
|
+
}],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
const wait = startLoaders(params);
|
|
70
|
+
await wait?.();
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
const currentMap = loaderDataStore.get(params.currentEntry.id);
|
|
74
|
+
expect(loader).not.toHaveBeenCalled();
|
|
75
|
+
expect(await currentMap?.get(loader)).toBe("old data");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("shouldReload が true を返すとき、ローダーが再実行される", async ({ expect }) => {
|
|
79
|
+
// Arrange
|
|
80
|
+
const loader: ILoader = async () => "new data";
|
|
81
|
+
const prevData = Promise.resolve("old data");
|
|
82
|
+
|
|
83
|
+
const loaderDataStore = createStubDataStore();
|
|
84
|
+
const prevEntryId = "8ce9bfcd-fe55-440a-80db-e90cb6e93ac5" as HistoryEntryId;
|
|
85
|
+
const prevMap = new Map();
|
|
86
|
+
prevMap.set(loader, prevData);
|
|
87
|
+
loaderDataStore.set(prevEntryId, prevMap);
|
|
88
|
+
|
|
89
|
+
const params = createParams({
|
|
90
|
+
prevEntry: {
|
|
91
|
+
id: prevEntryId,
|
|
92
|
+
url: new URL("http://localhost/"),
|
|
93
|
+
},
|
|
94
|
+
loaderDataStore,
|
|
95
|
+
currentRoutes: [
|
|
96
|
+
{
|
|
97
|
+
path: "/",
|
|
98
|
+
params: {},
|
|
99
|
+
dataFuncs: [{
|
|
100
|
+
action: undefined,
|
|
101
|
+
loader,
|
|
102
|
+
shouldAction: () => expect.unreachable(),
|
|
103
|
+
shouldReload: () => true,
|
|
104
|
+
}],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
const wait = startLoaders(params);
|
|
111
|
+
await wait?.();
|
|
112
|
+
|
|
113
|
+
// Assert
|
|
114
|
+
const currentMap = loaderDataStore.get(params.currentEntry.id);
|
|
115
|
+
expect(await currentMap?.get(loader)).toBe("new data");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("アクション実行後の挙動", () => {
|
|
120
|
+
test("アクション実行後に shouldReload が呼ばれるとき、triggerMethod が POST になる", async ({ expect }) => {
|
|
121
|
+
// Arrange
|
|
122
|
+
let capturedTriggerMethod: string | undefined;
|
|
123
|
+
const loader: ILoader = async () => "data";
|
|
124
|
+
const prevData = DeferredPromise.resolve("old");
|
|
125
|
+
const loaderDataStore = createStubDataStore();
|
|
126
|
+
const prevEntryId = "8ce9bfcd-fe55-440a-80db-e90cb6e93ac5" as HistoryEntryId;
|
|
127
|
+
loaderDataStore.set(prevEntryId, new Map([[loader, prevData]]));
|
|
128
|
+
|
|
129
|
+
const params = createParams({
|
|
130
|
+
prevEntry: {
|
|
131
|
+
id: prevEntryId,
|
|
132
|
+
url: new URL("http://localhost/"),
|
|
133
|
+
},
|
|
134
|
+
loaderDataStore,
|
|
135
|
+
formData: new FormData(),
|
|
136
|
+
actionResultMap: new Map(),
|
|
137
|
+
currentRoutes: [
|
|
138
|
+
{
|
|
139
|
+
path: "/",
|
|
140
|
+
params: {},
|
|
141
|
+
dataFuncs: [{
|
|
142
|
+
action: undefined,
|
|
143
|
+
loader,
|
|
144
|
+
shouldAction: () => expect.unreachable(),
|
|
145
|
+
shouldReload: ({ triggerMethod }) => {
|
|
146
|
+
capturedTriggerMethod = triggerMethod;
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
}],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Act
|
|
155
|
+
const wait = startLoaders(params);
|
|
156
|
+
await wait?.();
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
expect(capturedTriggerMethod).toBe("POST");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("アクションが実行された場合、前回のアクションデータが引き継がれる", ({ expect }) => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const action: IAction = async () => ({ success: true });
|
|
165
|
+
const actionData = DeferredPromise.resolve({ success: true });
|
|
166
|
+
const actionDataStore = createStubDataStore();
|
|
167
|
+
const prevEntryId = "8ce9bfcd-fe55-440a-80db-e90cb6e93ac5" as HistoryEntryId;
|
|
168
|
+
actionDataStore.set(prevEntryId, new Map([[action, actionData]]));
|
|
169
|
+
|
|
170
|
+
const params = createParams({
|
|
171
|
+
prevEntry: {
|
|
172
|
+
id: prevEntryId,
|
|
173
|
+
url: new URL("http://localhost/"),
|
|
174
|
+
},
|
|
175
|
+
actionDataStore,
|
|
176
|
+
formData: new FormData(),
|
|
177
|
+
actionResultMap: new Map(),
|
|
178
|
+
currentRoutes: [
|
|
179
|
+
{
|
|
180
|
+
path: "/",
|
|
181
|
+
params: {},
|
|
182
|
+
dataFuncs: [{
|
|
183
|
+
action,
|
|
184
|
+
loader: async () => {},
|
|
185
|
+
shouldAction: () => expect.unreachable(),
|
|
186
|
+
shouldReload: () => false,
|
|
187
|
+
}],
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Act
|
|
193
|
+
startLoaders(params);
|
|
194
|
+
|
|
195
|
+
// Assert
|
|
196
|
+
const currentActionMap = actionDataStore.get(params.currentEntry.id);
|
|
197
|
+
expect(currentActionMap?.get(action)).toBe(actionData);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("異常系", () => {
|
|
202
|
+
test("shouldReload が Promise を返したとき、LoaderConditionError が発生する", async ({ expect }) => {
|
|
203
|
+
// Arrange
|
|
204
|
+
const loader: ILoader = async () => "data";
|
|
205
|
+
const loaderDataStore = createStubDataStore();
|
|
206
|
+
const prevEntryId = "8ce9bfcd-fe55-440a-80db-e90cb6e93ac5" as HistoryEntryId;
|
|
207
|
+
loaderDataStore.set(prevEntryId, new Map([[loader, DeferredPromise.resolve("old")]]));
|
|
208
|
+
|
|
209
|
+
const params = createParams({
|
|
210
|
+
prevEntry: {
|
|
211
|
+
id: prevEntryId,
|
|
212
|
+
url: new URL("http://localhost/"),
|
|
213
|
+
},
|
|
214
|
+
loaderDataStore,
|
|
215
|
+
currentRoutes: [
|
|
216
|
+
{
|
|
217
|
+
path: "/",
|
|
218
|
+
params: {},
|
|
219
|
+
dataFuncs: [{
|
|
220
|
+
action: undefined,
|
|
221
|
+
loader,
|
|
222
|
+
shouldAction: () => expect.unreachable(),
|
|
223
|
+
// 意図的に非同期関数にする。
|
|
224
|
+
shouldReload: (async () => true) as any,
|
|
225
|
+
}],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Act
|
|
231
|
+
startLoaders(params);
|
|
232
|
+
|
|
233
|
+
// Assert
|
|
234
|
+
const currentMap = loaderDataStore.get(params.currentEntry.id);
|
|
235
|
+
const dataPromise = currentMap?.get(loader);
|
|
236
|
+
await expect(dataPromise).rejects.toThrow(LoaderConditionError);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* テスト用の StartLoadersParams を作成する。
|
|
242
|
+
*/
|
|
243
|
+
function createParams(overrides: Partial<StartLoadersParams> = {}): StartLoadersParams {
|
|
244
|
+
const params = {
|
|
245
|
+
prevRoutes: null,
|
|
246
|
+
currentRoutes: [],
|
|
247
|
+
prevEntry: {
|
|
248
|
+
id: "1e9a122d-ba56-4a57-9767-f8412ffc76f1" as HistoryEntryId,
|
|
249
|
+
url: new URL("http://localhost/"),
|
|
250
|
+
},
|
|
251
|
+
currentEntry: {
|
|
252
|
+
id: "d70c492b-7888-4a75-bf50-ec2ec758e643" as HistoryEntryId,
|
|
253
|
+
url: new URL("http://localhost/"),
|
|
254
|
+
},
|
|
255
|
+
actionDataStore: createStubDataStore(),
|
|
256
|
+
loaderDataStore: createStubDataStore(),
|
|
257
|
+
signal: new AbortController().signal,
|
|
258
|
+
...overrides,
|
|
259
|
+
} as StartLoadersParams;
|
|
260
|
+
if (params.prevEntry.id === params.currentEntry.id) {
|
|
261
|
+
throw new Error("同じエントリー ID: " + params.prevEntry.id);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return params;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* テスト用の IDataStore スタブを作成する。
|
|
269
|
+
*/
|
|
270
|
+
function createStubDataStore(): IDataStore<any> {
|
|
271
|
+
const store = new Map<string, Map<any, any>>();
|
|
272
|
+
return {
|
|
273
|
+
get: (id: string) => store.get(id),
|
|
274
|
+
set: (id: string, data: Map<any, any>) => store.set(id, data),
|
|
275
|
+
} as unknown as IDataStore<any>;
|
|
276
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, test, vi } from "vitest";
|
|
2
|
+
import processRoutes from "../../src/core/_process-routes.js";
|
|
3
|
+
import type { IDataStore } from "../../src/core/data-store.types.js";
|
|
4
|
+
import type { IAction, ILoader } from "../../src/core/route.types.js";
|
|
5
|
+
import NavigationApiEngine from "../../src/engines/navigation-api-engine.js";
|
|
6
|
+
|
|
7
|
+
let engine: NavigationApiEngine;
|
|
8
|
+
let mockNavigation: any;
|
|
9
|
+
let mockLoaderDataStore: IDataStore<ILoader>;
|
|
10
|
+
let mockActionDataStore: IDataStore<IAction>;
|
|
11
|
+
let abortController: AbortController;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
abortController = new AbortController();
|
|
15
|
+
|
|
16
|
+
// Navigation API のモックを作成する
|
|
17
|
+
mockNavigation = {
|
|
18
|
+
currentEntry: {
|
|
19
|
+
id: "59b2ba6e-5236-406c-9496-0a6bcd83b1ab",
|
|
20
|
+
url: "http://localhost/home",
|
|
21
|
+
index: 0,
|
|
22
|
+
addEventListener: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
entries: vi.fn().mockReturnValue([]),
|
|
25
|
+
addEventListener: vi.fn(),
|
|
26
|
+
navigate: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// グローバルオブジェクトに Navigation API を注入する
|
|
30
|
+
vi.stubGlobal("navigation", mockNavigation);
|
|
31
|
+
|
|
32
|
+
mockLoaderDataStore = {
|
|
33
|
+
has: vi.fn(),
|
|
34
|
+
get: vi.fn(),
|
|
35
|
+
set: vi.fn(),
|
|
36
|
+
delete: vi.fn(),
|
|
37
|
+
clear: vi.fn(),
|
|
38
|
+
} as any;
|
|
39
|
+
|
|
40
|
+
mockActionDataStore = {
|
|
41
|
+
has: vi.fn(),
|
|
42
|
+
get: vi.fn(),
|
|
43
|
+
set: vi.fn(),
|
|
44
|
+
delete: vi.fn(),
|
|
45
|
+
clear: vi.fn(),
|
|
46
|
+
} as any;
|
|
47
|
+
|
|
48
|
+
engine = new NavigationApiEngine();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("init", () => {
|
|
56
|
+
test("現在の URL にマッチするルートが存在する場合、初期状態を生成する", ({ expect }) => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const routes = processRoutes([
|
|
59
|
+
{ path: "/home" },
|
|
60
|
+
]);
|
|
61
|
+
const args = {
|
|
62
|
+
routes,
|
|
63
|
+
getSignal: () => abortController.signal,
|
|
64
|
+
loaderDataStore: mockLoaderDataStore,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
const state = engine.init(args);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(state).not.toBeNull();
|
|
72
|
+
expect(state?.entry.id).toBe("59b2ba6e-5236-406c-9496-0a6bcd83b1ab");
|
|
73
|
+
expect(state?.routes).toHaveLength(1);
|
|
74
|
+
expect(state?.routes[0]!.path).toBe("/home");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("現在の URL にマッチするルートが存在しない場合、null を返す", ({ expect }) => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const routes = processRoutes([
|
|
80
|
+
{ path: "/other" },
|
|
81
|
+
]);
|
|
82
|
+
const args = {
|
|
83
|
+
routes,
|
|
84
|
+
getSignal: () => abortController.signal,
|
|
85
|
+
loaderDataStore: mockLoaderDataStore,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Act
|
|
89
|
+
const state = engine.init(args);
|
|
90
|
+
|
|
91
|
+
// Assert
|
|
92
|
+
expect(state).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("start", () => {
|
|
97
|
+
test("開始したとき、navigate イベントリスナーを登録する", ({ expect }) => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const update = vi.fn();
|
|
100
|
+
const args = {
|
|
101
|
+
routes: [],
|
|
102
|
+
update,
|
|
103
|
+
getSignal: () => abortController.signal,
|
|
104
|
+
actionDataStore: mockActionDataStore,
|
|
105
|
+
loaderDataStore: mockLoaderDataStore,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Act
|
|
109
|
+
engine.start(args);
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect(mockNavigation.addEventListener).toHaveBeenCalledWith(
|
|
113
|
+
"navigate",
|
|
114
|
+
expect.any(Function),
|
|
115
|
+
expect.objectContaining({ signal: abortController.signal }),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("開始したとき、既存のエントリーに対して dispose リスナーを登録する", ({ expect }) => {
|
|
120
|
+
// Arrange
|
|
121
|
+
const mockEntry = {
|
|
122
|
+
id: "e88788c0-49b2-437b-a6b3-69c1bdbcc6da",
|
|
123
|
+
addEventListener: vi.fn(),
|
|
124
|
+
};
|
|
125
|
+
mockNavigation.entries.mockReturnValue([mockEntry]);
|
|
126
|
+
mockLoaderDataStore.has = vi.fn().mockReturnValue(true); // データが存在する場合のみ登録される
|
|
127
|
+
|
|
128
|
+
const args = {
|
|
129
|
+
routes: [],
|
|
130
|
+
update: vi.fn(),
|
|
131
|
+
getSignal: () => abortController.signal,
|
|
132
|
+
actionDataStore: mockActionDataStore,
|
|
133
|
+
loaderDataStore: mockLoaderDataStore,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Act
|
|
137
|
+
engine.start(args);
|
|
138
|
+
|
|
139
|
+
// Assert
|
|
140
|
+
expect(mockEntry.addEventListener).toHaveBeenCalledWith(
|
|
141
|
+
"dispose",
|
|
142
|
+
expect.any(Function),
|
|
143
|
+
expect.objectContaining({ signal: abortController.signal }),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("navigate", () => {
|
|
149
|
+
test("指定されたパスへ遷移を依頼したとき、Navigation API の navigate を呼び出す", ({ expect }) => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const navigateArgs = {
|
|
152
|
+
to: "/dashboard",
|
|
153
|
+
history: "push" as const,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Act
|
|
157
|
+
engine.navigate(navigateArgs);
|
|
158
|
+
|
|
159
|
+
// Assert
|
|
160
|
+
expect(mockNavigation.navigate).toHaveBeenCalledWith("/dashboard", { history: "push" });
|
|
161
|
+
});
|
|
162
|
+
});
|