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,108 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import isError from "../../src/core/_is-error.js";
|
|
3
|
+
|
|
4
|
+
describe("Error オブジェクトの判定", () => {
|
|
5
|
+
test("Error インスタンスを渡したとき、true になる", ({ expect }) => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const input = new Error("test error");
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const result = isError(input);
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(result).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("Error を継承したクラスのインスタンスを渡したとき、true になる", ({ expect }) => {
|
|
17
|
+
// Arrange
|
|
18
|
+
class CustomError extends Error {}
|
|
19
|
+
const input = new CustomError("custom error");
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
const result = isError(input);
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
expect(result).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// describe("Error に類似したオブジェクトの判定", () => {
|
|
30
|
+
// test("name と message を持つプレーンなオブジェクトを渡したとき、true になる", ({ expect }) => {
|
|
31
|
+
// // Arrange
|
|
32
|
+
// const input = { name: "CustomError", message: "something went wrong" };
|
|
33
|
+
|
|
34
|
+
// // Act
|
|
35
|
+
// const result = isError(input);
|
|
36
|
+
|
|
37
|
+
// // Assert
|
|
38
|
+
// expect(result).toBe(true);
|
|
39
|
+
// });
|
|
40
|
+
// });
|
|
41
|
+
|
|
42
|
+
describe("Error ではない値の判定", () => {
|
|
43
|
+
test("null を渡したとき、false になる", ({ expect }) => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const input = null;
|
|
46
|
+
|
|
47
|
+
// Act
|
|
48
|
+
const result = isError(input);
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(result).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("undefined を渡したとき、false になる", ({ expect }) => {
|
|
55
|
+
// Arrange
|
|
56
|
+
const input = undefined;
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
const result = isError(input);
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect(result).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("文字列を渡したとき、false になる", ({ expect }) => {
|
|
66
|
+
// Arrange
|
|
67
|
+
const input = "some error message";
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const result = isError(input);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("name のみが定義されたオブジェクトを渡したとき、false になる", ({ expect }) => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const input = { name: "Error" };
|
|
79
|
+
|
|
80
|
+
// Act
|
|
81
|
+
const result = isError(input);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
expect(result).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("message のみが定義されたオブジェクトを渡したとき、false になる", ({ expect }) => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const input = { message: "error occurred" };
|
|
90
|
+
|
|
91
|
+
// Act
|
|
92
|
+
const result = isError(input);
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(result).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("name が string ではないオブジェクトを渡したとき、false になる", ({ expect }) => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const input = { name: 123, message: "error" };
|
|
101
|
+
|
|
102
|
+
// Act
|
|
103
|
+
const result = isError(input);
|
|
104
|
+
|
|
105
|
+
// Assert
|
|
106
|
+
expect(result).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import isPromiseLike from "../../src/core/_is-promise-like.js";
|
|
3
|
+
|
|
4
|
+
describe("値が PromiseLike である場合", () => {
|
|
5
|
+
test("Promise インスタンスを渡したとき、 true を返す", ({ expect }) => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const value = Promise.resolve("test");
|
|
8
|
+
|
|
9
|
+
// Act
|
|
10
|
+
const result = isPromiseLike(value);
|
|
11
|
+
|
|
12
|
+
// Assert
|
|
13
|
+
expect(result).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("then メソッドを持つオブジェクトを渡したとき、 true を返す", ({ expect }) => {
|
|
17
|
+
// Arrange
|
|
18
|
+
const value = {
|
|
19
|
+
then: () => {},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Act
|
|
23
|
+
const result = isPromiseLike(value);
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(result).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("値が PromiseLike ではない場合", () => {
|
|
31
|
+
test("null を渡したとき、 false を返す", ({ expect }) => {
|
|
32
|
+
// Arrange
|
|
33
|
+
const value = null;
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const result = isPromiseLike(value);
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(result).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("undefined を渡したとき、 false を返す", ({ expect }) => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const value = undefined;
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
const result = isPromiseLike(value);
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("then プロパティを持たないオブジェクトを渡したとき、 false を返す", ({ expect }) => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const value = {
|
|
56
|
+
other: "property",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const result = isPromiseLike(value);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(result).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("then プロパティが関数ではないオブジェクトを渡したとき、 false を返す", ({ expect }) => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const value = {
|
|
69
|
+
then: "not a function",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
const result = isPromiseLike(value);
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(result).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("プリミティブな数値を渡したとき、 false を返す", ({ expect }) => {
|
|
80
|
+
// Arrange
|
|
81
|
+
const value = 123;
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
const result = isPromiseLike(value);
|
|
85
|
+
|
|
86
|
+
// Assert
|
|
87
|
+
expect(result).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("プリミティブな文字列を渡したとき、 false を返す", ({ expect }) => {
|
|
91
|
+
// Arrange
|
|
92
|
+
const value = "string";
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const result = isPromiseLike(value);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(result).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import matchPath from "../../src/core/_match-route-path.js";
|
|
3
|
+
|
|
4
|
+
describe("パス名がルートのパターンに合致する場合", () => {
|
|
5
|
+
test("パスから動的パラメーターが抽出され、MatchPathResult オブジェクトが返される", ({ expect }) => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const route = {
|
|
8
|
+
// path: "/users/:id",
|
|
9
|
+
pathPattern: /^\/users\/([^/]+?)(?:\/)?$/i,
|
|
10
|
+
paramKeys: ["id"],
|
|
11
|
+
};
|
|
12
|
+
const pathname = "/users/123";
|
|
13
|
+
|
|
14
|
+
// Act
|
|
15
|
+
const result = matchPath(route, pathname);
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(result).not.toBeNull();
|
|
19
|
+
expect(result?.params).toStrictEqual({ id: "123" });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("複数の動的パラメーターが含まれるとき、すべてのパラメーターが正しく抽出される", ({ expect }) => {
|
|
23
|
+
// Arrange
|
|
24
|
+
const route = {
|
|
25
|
+
// path: "/posts/:year/:month",
|
|
26
|
+
pathPattern: /^\/posts\/([^/]+?)\/([^/]+?)(?:\/)?$/i,
|
|
27
|
+
paramKeys: ["year", "month"],
|
|
28
|
+
};
|
|
29
|
+
const pathname = "/posts/2025/12";
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const result = matchPath(route, pathname);
|
|
33
|
+
|
|
34
|
+
// Assert
|
|
35
|
+
expect(result?.params).toStrictEqual({
|
|
36
|
+
year: "2025",
|
|
37
|
+
month: "12",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("パスに動的パラメーターが含まれないとき、空の params を持つオブジェクトが返される", ({ expect }) => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const route = {
|
|
44
|
+
// path: "/about",
|
|
45
|
+
pathPattern: /^\/about(?:\/)?$/i,
|
|
46
|
+
paramKeys: [],
|
|
47
|
+
};
|
|
48
|
+
const pathname = "/about";
|
|
49
|
+
|
|
50
|
+
// Act
|
|
51
|
+
const result = matchPath(route, pathname);
|
|
52
|
+
|
|
53
|
+
// Assert
|
|
54
|
+
expect(result?.params).toStrictEqual({});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("パス名がルートのパターンに合致しない場合", () => {
|
|
59
|
+
test("マッチングに失敗したとき、null が返される", ({ expect }) => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const route = {
|
|
62
|
+
// path: "/users/:id",
|
|
63
|
+
pathPattern: /^\/users\/([^/]+?)(?:\/)?$/i,
|
|
64
|
+
paramKeys: ["id"],
|
|
65
|
+
};
|
|
66
|
+
const pathname = "/other/123";
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
const result = matchPath(route, pathname);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import preprocessRoutes from "../../src/core/_process-routes.js";
|
|
3
|
+
import type { RouteDefinition } from "../../src/core/route.types.js";
|
|
4
|
+
|
|
5
|
+
describe("パスの正規化とマッチング", () => {
|
|
6
|
+
test("階層構造を持つルート定義を渡したとき、親のパスを引き継いで正規化される", ({ expect }) => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const routes: RouteDefinition[] = [
|
|
9
|
+
{
|
|
10
|
+
path: "parent",
|
|
11
|
+
children: [
|
|
12
|
+
{ path: "child" },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Act
|
|
18
|
+
const result = preprocessRoutes(routes);
|
|
19
|
+
|
|
20
|
+
// Assert
|
|
21
|
+
expect(result[0]?.path).toBe("/parent");
|
|
22
|
+
expect(result[0]?.children[0]?.path).toBe("/parent/child");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("ルート定義が入れ子になっているとき、パスパターンの正規表現が適切に生成される", ({ expect }) => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const routes: RouteDefinition[] = [
|
|
28
|
+
{
|
|
29
|
+
path: "/users",
|
|
30
|
+
children: [
|
|
31
|
+
{ path: ":id" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const result = preprocessRoutes(routes);
|
|
38
|
+
const childRoute = result[0]?.children[0];
|
|
39
|
+
|
|
40
|
+
// Assert
|
|
41
|
+
expect(childRoute?.pathPattern).toBeInstanceOf(RegExp);
|
|
42
|
+
expect(childRoute?.pathPattern.test("/users/123")).toBe(true);
|
|
43
|
+
expect(childRoute?.paramKeys).toStrictEqual(["id"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("複数のルートが存在するとき、パスの文字列順でソートされる", ({ expect }) => {
|
|
47
|
+
// Arrange
|
|
48
|
+
const routes: RouteDefinition[] = [
|
|
49
|
+
{ path: "/zebra" },
|
|
50
|
+
{ path: "/apple" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Act
|
|
54
|
+
const result = preprocessRoutes(routes);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(result[0]?.path).toBe("/apple");
|
|
58
|
+
expect(result[1]?.path).toBe("/zebra");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("データ操作関数の補完", () => {
|
|
63
|
+
test("dataFunctions が未定義の場合、空の配列として処理される", ({ expect }) => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const routes: RouteDefinition[] = [
|
|
66
|
+
{ path: "/" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const result = preprocessRoutes(routes);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result[0]?.dataFuncs).toStrictEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("shouldAction または shouldReload が未定義のとき、常にデフォルト判定を返す関数が設定される", ({ expect }) => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const routes: RouteDefinition[] = [
|
|
79
|
+
{
|
|
80
|
+
path: "/",
|
|
81
|
+
dataFunctions: [
|
|
82
|
+
{
|
|
83
|
+
loader: async () => ({}),
|
|
84
|
+
// shouldReload と shouldAction は省略
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
const result = preprocessRoutes(routes);
|
|
92
|
+
const dataFunc = result[0]?.dataFuncs[0];
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(dataFunc?.shouldAction({ defaultShouldAction: true } as any)).toBe(true);
|
|
96
|
+
expect(dataFunc?.shouldReload({ defaultShouldReload: false } as any)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("定義された action や loader がそのまま保持される", ({ expect }) => {
|
|
100
|
+
// Arrange
|
|
101
|
+
const mockLoader = async () => ({ message: "hello" });
|
|
102
|
+
const routes: RouteDefinition[] = [
|
|
103
|
+
{
|
|
104
|
+
path: "/",
|
|
105
|
+
dataFunctions: [{ loader: mockLoader }],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
const result = preprocessRoutes(routes);
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect(result[0]?.dataFuncs[0]?.loader).toBe(mockLoader);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("ルートの属性判定", () => {
|
|
118
|
+
test("子ルートを持たないルートは、index 属性が true になる", ({ expect }) => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const routes: RouteDefinition[] = [
|
|
121
|
+
{ path: "/leaf" },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// Act
|
|
125
|
+
const result = preprocessRoutes(routes);
|
|
126
|
+
|
|
127
|
+
// Assert
|
|
128
|
+
expect(result[0]?.index).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("子ルートを持つルートは、index 属性が false になる", ({ expect }) => {
|
|
132
|
+
// Arrange
|
|
133
|
+
const routes: RouteDefinition[] = [
|
|
134
|
+
{
|
|
135
|
+
path: "/parent",
|
|
136
|
+
children: [{ path: "child" }],
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Act
|
|
141
|
+
const result = preprocessRoutes(routes);
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
expect(result[0]?.index).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { afterEach, describe, test } from "vitest";
|
|
2
|
+
import singleton from "../../src/core/_singleton.js";
|
|
3
|
+
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
// テスト後にキャッシュをクリーンアップする。
|
|
6
|
+
globalThis.soseki__singleton = undefined;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("同期関数の場合", () => {
|
|
10
|
+
test("初回呼び出しのとき、ファクトリー関数の戻り値を返す", ({ expect }) => {
|
|
11
|
+
// Arrange
|
|
12
|
+
const key = "test-key";
|
|
13
|
+
const expectedValue = { id: 1 };
|
|
14
|
+
|
|
15
|
+
// Act
|
|
16
|
+
const result = singleton(key, () => expectedValue);
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(result).toBe(expectedValue);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("同じキーで複数回呼び出したとき、常に最初に生成されたインスタンスを返す", ({ expect }) => {
|
|
23
|
+
// Arrange
|
|
24
|
+
const key = "stable-key";
|
|
25
|
+
let callCount = 0;
|
|
26
|
+
const factory = () => {
|
|
27
|
+
callCount++;
|
|
28
|
+
return { count: callCount };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Act
|
|
32
|
+
const firstResult = singleton(key, factory);
|
|
33
|
+
const secondResult = singleton(key, factory);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(secondResult).toBe(firstResult);
|
|
37
|
+
expect(callCount).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("異なるキーで呼び出したとき、それぞれ個別のインスタンスを返す", ({ expect }) => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const keyA = "key-a";
|
|
43
|
+
const keyB = "key-b";
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const resultA = singleton(keyA, () => ({ name: "A" }));
|
|
47
|
+
const resultB = singleton(keyB, () => ({ name: "B" }));
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(resultA).not.toBe(resultB);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("非同期関数の場合", () => {
|
|
55
|
+
test("初回呼び出しのとき、解決された値を返す Promise を返す", async ({ expect }) => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const key = "async-key";
|
|
58
|
+
const expectedValue = "resolved-value";
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
const resultPromise = singleton(key, async () => expectedValue);
|
|
62
|
+
const result = await resultPromise;
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(result).toBe(expectedValue);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("解決待ちの間に再度呼び出されたとき、同一の Promise インスタンスを返す", ({ expect }) => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const key = "pending-key";
|
|
71
|
+
const factory = async () => "value";
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
const firstPromise = singleton(key, factory);
|
|
75
|
+
const secondPromise = singleton(key, factory);
|
|
76
|
+
|
|
77
|
+
// Assert
|
|
78
|
+
expect(firstPromise).toBe(secondPromise);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("Promise が拒絶されたとき、次に呼び出された際にファクトリー関数を再実行する", async ({ expect }) => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const key = "error-key";
|
|
84
|
+
let callCount = 0;
|
|
85
|
+
const factory = async () => {
|
|
86
|
+
callCount++;
|
|
87
|
+
if (callCount === 1) {
|
|
88
|
+
throw new Error("初回失敗");
|
|
89
|
+
}
|
|
90
|
+
return "成功";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Act & Assert
|
|
94
|
+
// 1 回目の呼び出し:エラーになることを確認する。
|
|
95
|
+
await expect(singleton(key, factory)).rejects.toThrow("初回失敗");
|
|
96
|
+
|
|
97
|
+
// 2 回目の呼び出し:キャッシュが削除されており、再試行されることを確認する。
|
|
98
|
+
const secondResult = await singleton(key, factory);
|
|
99
|
+
expect(secondResult).toBe("成功");
|
|
100
|
+
expect(callCount).toBe(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
import unreachable from "../../src/core/_unreachable.js";
|
|
3
|
+
import { UnreachableError } from "../../src/core/errors.js";
|
|
4
|
+
|
|
5
|
+
describe("引数なしで呼び出した場合", () => {
|
|
6
|
+
test("UnreachableError がスローされる", ({ expect }) => {
|
|
7
|
+
// Arrange
|
|
8
|
+
const act = () => {
|
|
9
|
+
unreachable();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Act & Assert
|
|
13
|
+
expect(act).toThrow(UnreachableError);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("switch 文の網羅性チェックなどで予期しない値が渡された場合", () => {
|
|
18
|
+
test("渡された値を含む UnreachableError がスローされる", ({ expect }) => {
|
|
19
|
+
// Arrange
|
|
20
|
+
type Shape = "circle" | "square";
|
|
21
|
+
const shape = "triangle" as unknown as Shape;
|
|
22
|
+
|
|
23
|
+
// Act
|
|
24
|
+
const act = () => {
|
|
25
|
+
switch (shape) {
|
|
26
|
+
case "circle":
|
|
27
|
+
return 1;
|
|
28
|
+
case "square":
|
|
29
|
+
return 2;
|
|
30
|
+
default:
|
|
31
|
+
return unreachable(shape);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(act).toThrow(UnreachableError);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { test, vi } from "vitest";
|
|
2
|
+
import { renderHook } from "vitest-browser-react";
|
|
3
|
+
import useSingleton from "../../src/core/_use-singleton.js";
|
|
4
|
+
|
|
5
|
+
test("最初のレンダリング時に、初期化関数が実行されてその結果を返す", async ({ expect }) => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const expectedValue = "initial value";
|
|
8
|
+
const initFn = () => expectedValue;
|
|
9
|
+
|
|
10
|
+
// Act
|
|
11
|
+
const { result } = await renderHook(() => useSingleton(initFn));
|
|
12
|
+
|
|
13
|
+
// Assert
|
|
14
|
+
expect(result.current).toBe(expectedValue);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("再レンダリングが発生しても、初期化関数は二度目以降の実行はされない", async ({ expect }) => {
|
|
18
|
+
// Arrange
|
|
19
|
+
const initFn = vi.fn(() => ({ id: Math.random() }));
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
const { rerender } = await renderHook(() => useSingleton(initFn));
|
|
23
|
+
|
|
24
|
+
// 再レンダリングを実行する。
|
|
25
|
+
await rerender();
|
|
26
|
+
await rerender();
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(initFn).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("再レンダリングが発生しても、最初に生成されたオブジェクトと同一のインスタンスを返し続ける", async ({ expect }) => {
|
|
33
|
+
// Arrange
|
|
34
|
+
// 毎回新しいオブジェクトを生成する初期化関数を準備
|
|
35
|
+
const initFn = () => ({ timestamp: Date.now() });
|
|
36
|
+
|
|
37
|
+
// Act
|
|
38
|
+
const { result, rerender } = await renderHook(() => useSingleton(initFn));
|
|
39
|
+
const firstResult = result.current;
|
|
40
|
+
|
|
41
|
+
await rerender();
|
|
42
|
+
const secondResult = result.current;
|
|
43
|
+
|
|
44
|
+
// Assert
|
|
45
|
+
// 参照が同一であることを確認し、シングルトンとしての振る舞いを保証
|
|
46
|
+
expect(secondResult).toBe(firstResult);
|
|
47
|
+
});
|