@mandujs/core 0.20.9 → 0.21.0

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.
@@ -1,189 +1,189 @@
1
- /**
2
- * Mandu Testing Utilities
3
- * 서버 없이 라우트/filling 단위 테스트
4
- */
5
-
6
- import { ManduContext } from "../filling/context";
7
- import type { ManduFilling } from "../filling/filling";
8
- import type { RouteSpec, RoutesManifest } from "../spec/schema";
9
-
10
- // ========== Types ==========
11
-
12
- export interface TestRequestOptions {
13
- method?: string;
14
- query?: Record<string, string>;
15
- body?: unknown;
16
- headers?: Record<string, string>;
17
- params?: Record<string, string>;
18
- /** Action 이름 — 자동으로 _action을 body에 삽입하고 ManduAction 헤더를 추가 */
19
- action?: string;
20
- }
21
-
22
- // ========== testFilling ==========
23
-
24
- /**
25
- * Filling 단위 테스트 — 서버 없이 직접 실행
26
- *
27
- * @example
28
- * ```typescript
29
- * import { testFilling } from "@mandujs/core/testing";
30
- * import todoRoute from "./app/api/todos/route";
31
- *
32
- * const res = await testFilling(todoRoute, {
33
- * method: "GET",
34
- * query: { page: "2" },
35
- * });
36
- * expect(res.status).toBe(200);
37
- *
38
- * const data = await res.json();
39
- * expect(data.todos).toHaveLength(10);
40
- * ```
41
- */
42
- export async function testFilling(
43
- filling: ManduFilling,
44
- options: TestRequestOptions = {}
45
- ): Promise<Response> {
46
- const {
47
- method: rawMethod,
48
- query,
49
- body: rawBody,
50
- headers: rawHeaders = {},
51
- params = {},
52
- action,
53
- } = options;
54
-
55
- // action 지정 시 자동으로 POST + _action body + ManduAction 헤더
56
- const method = rawMethod ?? (action ? "POST" : "GET");
57
- const headers = { ...rawHeaders };
58
- let body = rawBody;
59
-
60
- if (action) {
61
- headers["X-Requested-With"] = "ManduAction";
62
- headers["Accept"] = "application/json";
63
- if (body && typeof body === "object" && !(body instanceof FormData)) {
64
- body = { _action: action, ...(body as Record<string, unknown>) };
65
- } else if (!body) {
66
- body = { _action: action };
67
- }
68
- }
69
-
70
- const url = new URL("http://localhost/test");
71
- if (query) {
72
- for (const [key, value] of Object.entries(query)) {
73
- url.searchParams.set(key, value);
74
- }
75
- }
76
-
77
- const requestInit: RequestInit = {
78
- method,
79
- headers,
80
- };
81
-
82
- if (body !== undefined && method !== "GET" && method !== "HEAD") {
83
- if (body instanceof FormData) {
84
- requestInit.body = body;
85
- } else {
86
- requestInit.body = JSON.stringify(body);
87
- (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
88
- }
89
- }
90
-
91
- const request = new Request(url.toString(), requestInit);
92
- return filling.handle(request, params);
93
- }
94
-
95
- /**
96
- * 간단한 Request 생성 헬퍼
97
- *
98
- * @example
99
- * ```typescript
100
- * const req = createTestRequest("/api/todos", { method: "POST", body: { title: "test" } });
101
- * ```
102
- */
103
- export function createTestRequest(
104
- path: string,
105
- options: TestRequestOptions = {}
106
- ): Request {
107
- const { method = "GET", query, body, headers = {} } = options;
108
-
109
- const url = new URL(`http://localhost${path}`);
110
- if (query) {
111
- for (const [key, value] of Object.entries(query)) {
112
- url.searchParams.set(key, value);
113
- }
114
- }
115
-
116
- const requestInit: RequestInit = { method, headers: { ...headers } };
117
-
118
- if (body !== undefined && method !== "GET" && method !== "HEAD") {
119
- if (body instanceof FormData) {
120
- requestInit.body = body;
121
- } else {
122
- requestInit.body = JSON.stringify(body);
123
- (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
124
- }
125
- }
126
-
127
- return new Request(url.toString(), requestInit);
128
- }
129
-
130
- /**
131
- * ManduContext 테스트용 생성 헬퍼
132
- *
133
- * @example
134
- * ```typescript
135
- * const ctx = createTestContext("/api/users/123", { params: { id: "123" } });
136
- * expect(ctx.params.id).toBe("123");
137
- * ```
138
- */
139
- export function createTestContext(
140
- path: string,
141
- options: TestRequestOptions = {}
142
- ): ManduContext {
143
- const request = createTestRequest(path, options);
144
- return new ManduContext(request, options.params);
145
- }
146
-
147
- // ========== Test Factories ==========
148
-
149
- /**
150
- * Create a RoutesManifest from partial route definitions.
151
- * Fills in sensible defaults so tests only specify the fields they care about.
152
- *
153
- * @example
154
- * ```typescript
155
- * const manifest = createTestManifest([
156
- * { id: "home", kind: "page", pattern: "/" },
157
- * { id: "api-users", kind: "api", pattern: "/api/users" },
158
- * ]);
159
- * ```
160
- */
161
- export function createTestManifest(routes: Partial<RouteSpec>[]): RoutesManifest {
162
- return {
163
- version: 1,
164
- routes: routes.map((r, i) => ({
165
- id: r.id ?? `test-route-${i}`,
166
- kind: r.kind ?? "page",
167
- pattern: r.pattern ?? `/test-${i}`,
168
- module: r.module ?? `app/test-${i}/page.tsx`,
169
- componentModule:
170
- (r.kind ?? "page") === "page"
171
- ? (r.componentModule ?? r.module ?? `app/test-${i}/page.tsx`)
172
- : undefined,
173
- ...r,
174
- })) as RouteSpec[],
175
- };
176
- }
177
-
178
- /**
179
- * Create a minimal island descriptor for testing hydration logic.
180
- *
181
- * @example
182
- * ```typescript
183
- * const island = createTestIsland("counter", "interaction");
184
- * expect(island.__hydrate).toBe("interaction");
185
- * ```
186
- */
187
- export function createTestIsland(name: string, strategy: string = "visible") {
188
- return { __island: true, __hydrate: strategy, __name: name };
189
- }
1
+ /**
2
+ * Mandu Testing Utilities
3
+ * 서버 없이 라우트/filling 단위 테스트
4
+ */
5
+
6
+ import { ManduContext } from "../filling/context";
7
+ import type { ManduFilling } from "../filling/filling";
8
+ import type { RouteSpec, RoutesManifest } from "../spec/schema";
9
+
10
+ // ========== Types ==========
11
+
12
+ export interface TestRequestOptions {
13
+ method?: string;
14
+ query?: Record<string, string>;
15
+ body?: unknown;
16
+ headers?: Record<string, string>;
17
+ params?: Record<string, string>;
18
+ /** Action 이름 — 자동으로 _action을 body에 삽입하고 ManduAction 헤더를 추가 */
19
+ action?: string;
20
+ }
21
+
22
+ // ========== testFilling ==========
23
+
24
+ /**
25
+ * Filling 단위 테스트 — 서버 없이 직접 실행
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { testFilling } from "@mandujs/core/testing";
30
+ * import todoRoute from "./app/api/todos/route";
31
+ *
32
+ * const res = await testFilling(todoRoute, {
33
+ * method: "GET",
34
+ * query: { page: "2" },
35
+ * });
36
+ * expect(res.status).toBe(200);
37
+ *
38
+ * const data = await res.json();
39
+ * expect(data.todos).toHaveLength(10);
40
+ * ```
41
+ */
42
+ export async function testFilling(
43
+ filling: ManduFilling,
44
+ options: TestRequestOptions = {}
45
+ ): Promise<Response> {
46
+ const {
47
+ method: rawMethod,
48
+ query,
49
+ body: rawBody,
50
+ headers: rawHeaders = {},
51
+ params = {},
52
+ action,
53
+ } = options;
54
+
55
+ // action 지정 시 자동으로 POST + _action body + ManduAction 헤더
56
+ const method = rawMethod ?? (action ? "POST" : "GET");
57
+ const headers = { ...rawHeaders };
58
+ let body = rawBody;
59
+
60
+ if (action) {
61
+ headers["X-Requested-With"] = "ManduAction";
62
+ headers["Accept"] = "application/json";
63
+ if (body && typeof body === "object" && !(body instanceof FormData)) {
64
+ body = { _action: action, ...(body as Record<string, unknown>) };
65
+ } else if (!body) {
66
+ body = { _action: action };
67
+ }
68
+ }
69
+
70
+ const url = new URL("http://localhost/test");
71
+ if (query) {
72
+ for (const [key, value] of Object.entries(query)) {
73
+ url.searchParams.set(key, value);
74
+ }
75
+ }
76
+
77
+ const requestInit: RequestInit = {
78
+ method,
79
+ headers,
80
+ };
81
+
82
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
83
+ if (body instanceof FormData) {
84
+ requestInit.body = body;
85
+ } else {
86
+ requestInit.body = JSON.stringify(body);
87
+ (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
88
+ }
89
+ }
90
+
91
+ const request = new Request(url.toString(), requestInit);
92
+ return filling.handle(request, params);
93
+ }
94
+
95
+ /**
96
+ * 간단한 Request 생성 헬퍼
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const req = createTestRequest("/api/todos", { method: "POST", body: { title: "test" } });
101
+ * ```
102
+ */
103
+ export function createTestRequest(
104
+ path: string,
105
+ options: TestRequestOptions = {}
106
+ ): Request {
107
+ const { method = "GET", query, body, headers = {} } = options;
108
+
109
+ const url = new URL(`http://localhost${path}`);
110
+ if (query) {
111
+ for (const [key, value] of Object.entries(query)) {
112
+ url.searchParams.set(key, value);
113
+ }
114
+ }
115
+
116
+ const requestInit: RequestInit = { method, headers: { ...headers } };
117
+
118
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
119
+ if (body instanceof FormData) {
120
+ requestInit.body = body;
121
+ } else {
122
+ requestInit.body = JSON.stringify(body);
123
+ (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
124
+ }
125
+ }
126
+
127
+ return new Request(url.toString(), requestInit);
128
+ }
129
+
130
+ /**
131
+ * ManduContext 테스트용 생성 헬퍼
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const ctx = createTestContext("/api/users/123", { params: { id: "123" } });
136
+ * expect(ctx.params.id).toBe("123");
137
+ * ```
138
+ */
139
+ export function createTestContext(
140
+ path: string,
141
+ options: TestRequestOptions = {}
142
+ ): ManduContext {
143
+ const request = createTestRequest(path, options);
144
+ return new ManduContext(request, options.params);
145
+ }
146
+
147
+ // ========== Test Factories ==========
148
+
149
+ /**
150
+ * Create a RoutesManifest from partial route definitions.
151
+ * Fills in sensible defaults so tests only specify the fields they care about.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * const manifest = createTestManifest([
156
+ * { id: "home", kind: "page", pattern: "/" },
157
+ * { id: "api-users", kind: "api", pattern: "/api/users" },
158
+ * ]);
159
+ * ```
160
+ */
161
+ export function createTestManifest(routes: Partial<RouteSpec>[]): RoutesManifest {
162
+ return {
163
+ version: 1,
164
+ routes: routes.map((r, i) => ({
165
+ id: r.id ?? `test-route-${i}`,
166
+ kind: r.kind ?? "page",
167
+ pattern: r.pattern ?? `/test-${i}`,
168
+ module: r.module ?? `app/test-${i}/page.tsx`,
169
+ componentModule:
170
+ (r.kind ?? "page") === "page"
171
+ ? (r.componentModule ?? r.module ?? `app/test-${i}/page.tsx`)
172
+ : undefined,
173
+ ...r,
174
+ })) as RouteSpec[],
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Create a minimal island descriptor for testing hydration logic.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const island = createTestIsland("counter", "interaction");
184
+ * expect(island.__hydrate).toBe("interaction");
185
+ * ```
186
+ */
187
+ export function createTestIsland(name: string, strategy: string = "visible") {
188
+ return { __island: true, __hydrate: strategy, __name: name };
189
+ }