@chidchanun/bcp 0.2.13 → 0.2.14

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.
@@ -0,0 +1,52 @@
1
+ export {
2
+ createFakeClock,
3
+ createJobTestHarness,
4
+ createOutboxTestHarness,
5
+ createRealtimeTestHarness,
6
+ createRealtimeTestSocket,
7
+ createRouteTestHandler,
8
+ createSequenceIdFactory,
9
+ createTestApp,
10
+ createTestAuthSession,
11
+ createWorkflowTestHarness,
12
+ expectResponse,
13
+ readSseEvents,
14
+ runTestMiddleware,
15
+ withTestTransaction,
16
+ } from "../../server/src/testing.js";
17
+
18
+ export {
19
+ createTestFormData,
20
+ runTestPageAction,
21
+ runTestPageGuards,
22
+ runTestPageLoader,
23
+ } from "../../server/src/testing-page.js";
24
+
25
+ export type {
26
+ CreateTestAuthSessionOptions,
27
+ FakeClock,
28
+ JobTestHarness,
29
+ OutboxTestHarness,
30
+ ParsedSseEvent,
31
+ RealtimeTestConnection,
32
+ RealtimeTestHarness,
33
+ RealtimeTestSocket,
34
+ TestApp,
35
+ TestAppOptions,
36
+ TestAuthSession,
37
+ TestRequestHandler,
38
+ TestRequestOptions,
39
+ TestResponseExpectation,
40
+ TestRouteFunction,
41
+ TestRouteHandlerOptions,
42
+ TestRouteMethod,
43
+ TestRouteModule,
44
+ TestTransactionDatabase,
45
+ WorkflowTestHarness,
46
+ } from "../../server/src/testing.js";
47
+
48
+ export type {
49
+ TestPageActionOptions,
50
+ TestPageContextOptions,
51
+ TestPageGuardOptions,
52
+ } from "../../server/src/testing-page.js";
@@ -1139,43 +1139,88 @@ export function createRealtimeSseResponse(
1139
1139
  options.keepAliveMs ?? 15_000,
1140
1140
  "SSE keepAliveMs"
1141
1141
  );
1142
+ let closed = false;
1143
+ let controller:
1144
+ ReadableStreamDefaultController<Uint8Array> |
1145
+ undefined;
1146
+ let timer:
1147
+ ReturnType<typeof setTimeout> |
1148
+ undefined;
1149
+ let unsubscribe:
1150
+ () => void =
1151
+ () => undefined;
1152
+ let abortListener:
1153
+ (() => void) |
1154
+ undefined;
1155
+
1156
+ const cleanup = () => {
1157
+ if (closed) {
1158
+ return false;
1159
+ }
1160
+ closed = true;
1161
+ if (timer) {
1162
+ clearTimeout(timer);
1163
+ timer = undefined;
1164
+ }
1165
+ unsubscribe();
1166
+ unsubscribe =
1167
+ () => undefined;
1168
+ if (
1169
+ options.signal &&
1170
+ abortListener
1171
+ ) {
1172
+ options.signal.removeEventListener(
1173
+ "abort",
1174
+ abortListener
1175
+ );
1176
+ abortListener = undefined;
1177
+ }
1178
+ return true;
1179
+ };
1180
+
1181
+ const close = () => {
1182
+ if (!cleanup()) {
1183
+ return;
1184
+ }
1185
+ const currentController =
1186
+ controller;
1187
+ controller = undefined;
1188
+ if (!currentController) {
1189
+ return;
1190
+ }
1191
+ try {
1192
+ currentController.close();
1193
+ } catch {
1194
+ // Stream may already be closed or cancelled by the runtime.
1195
+ }
1196
+ };
1197
+
1198
+ const write = (
1199
+ value: string
1200
+ ) => {
1201
+ const currentController =
1202
+ controller;
1203
+ if (
1204
+ closed ||
1205
+ !currentController
1206
+ ) {
1207
+ return;
1208
+ }
1209
+ try {
1210
+ currentController.enqueue(
1211
+ encoder.encode(value)
1212
+ );
1213
+ } catch {
1214
+ cleanup();
1215
+ controller = undefined;
1216
+ }
1217
+ };
1142
1218
 
1143
1219
  const stream =
1144
1220
  new ReadableStream<Uint8Array>({
1145
- start(controller) {
1146
- let closed = false;
1147
- let timer:
1148
- ReturnType<typeof setTimeout> |
1149
- undefined;
1150
- let unsubscribe:
1151
- () => void =
1152
- () => undefined;
1153
-
1154
- const close = () => {
1155
- if (closed) {
1156
- return;
1157
- }
1158
- closed = true;
1159
- if (timer) {
1160
- clearTimeout(timer);
1161
- }
1162
- unsubscribe();
1163
- try {
1164
- controller.close();
1165
- } catch {
1166
- // Stream may already be closed by the runtime.
1167
- }
1168
- };
1169
-
1170
- const write = (
1171
- value: string
1172
- ) => {
1173
- if (!closed) {
1174
- controller.enqueue(
1175
- encoder.encode(value)
1176
- );
1177
- }
1178
- };
1221
+ start(streamController) {
1222
+ controller =
1223
+ streamController;
1179
1224
 
1180
1225
  if (retryMs !== undefined) {
1181
1226
  write(
@@ -1211,11 +1256,13 @@ export function createRealtimeSseResponse(
1211
1256
  write(
1212
1257
  `: keep-alive ${Date.now()}\n\n`
1213
1258
  );
1214
- timer =
1215
- setTimeout(
1216
- keepAlive,
1217
- keepAliveMs
1218
- );
1259
+ if (!closed) {
1260
+ timer =
1261
+ setTimeout(
1262
+ keepAlive,
1263
+ keepAliveMs
1264
+ );
1265
+ }
1219
1266
  };
1220
1267
  timer =
1221
1268
  setTimeout(
@@ -1224,19 +1271,26 @@ export function createRealtimeSseResponse(
1224
1271
  );
1225
1272
 
1226
1273
  if (options.signal) {
1274
+ abortListener =
1275
+ close;
1227
1276
  if (options.signal.aborted) {
1228
1277
  close();
1229
1278
  return;
1230
1279
  }
1231
1280
  options.signal.addEventListener(
1232
1281
  "abort",
1233
- close,
1282
+ abortListener,
1234
1283
  {
1235
1284
  once: true,
1236
1285
  }
1237
1286
  );
1238
1287
  }
1239
1288
  },
1289
+
1290
+ cancel() {
1291
+ cleanup();
1292
+ controller = undefined;
1293
+ },
1240
1294
  });
1241
1295
 
1242
1296
  const headers =
@@ -1461,4 +1515,4 @@ function formatError(
1461
1515
  } catch {
1462
1516
  return String(error);
1463
1517
  }
1464
- }
1518
+ }
@@ -0,0 +1,274 @@
1
+ import {
2
+ executePageActionFunction,
3
+ normalizePageActionMethod,
4
+ type PageAction,
5
+ type PageActionExecution,
6
+ type PageActionMethod,
7
+ } from "./form-action.js";
8
+ import {
9
+ executePageGuardFunctions,
10
+ type PageGuard,
11
+ type PageGuardData,
12
+ type PageGuardDefinition,
13
+ type PageGuardExecution,
14
+ } from "./page-guard.js";
15
+ import {
16
+ executePageLoaderFunction,
17
+ type PageLoader,
18
+ type PageLoaderExecution,
19
+ } from "./page-loader.js";
20
+
21
+ export interface TestPageContextOptions {
22
+ params?: Record<string, any>;
23
+ url?: string | URL;
24
+ guardData?: PageGuardData;
25
+ hasGuard?: boolean;
26
+ }
27
+
28
+ export interface TestPageGuardOptions
29
+ extends Omit<
30
+ TestPageContextOptions,
31
+ "guardData" | "hasGuard"
32
+ > {
33
+ label?: string;
34
+ }
35
+
36
+ export interface TestPageActionOptions
37
+ extends TestPageContextOptions {
38
+ actionName?: string;
39
+ method?: PageActionMethod | string;
40
+ formData?: FormData;
41
+ fields?: Record<
42
+ string,
43
+ string | Blob | Array<string | Blob>
44
+ >;
45
+ label?: string;
46
+ }
47
+
48
+ export async function runTestPageLoader(
49
+ loader: PageLoader,
50
+ options: TestPageContextOptions = {}
51
+ ): Promise<PageLoaderExecution> {
52
+ assertFunction(
53
+ loader,
54
+ "page loader"
55
+ );
56
+
57
+ return executePageLoaderFunction(
58
+ loader,
59
+ {
60
+ ...(options.params ?? {}),
61
+ },
62
+ resolveTestPageUrl(
63
+ options.url
64
+ ),
65
+ "bcp/testing loader",
66
+ {
67
+ ...(options.guardData ?? {}),
68
+ },
69
+ options.hasGuard ??
70
+ Object.keys(
71
+ options.guardData ?? {}
72
+ ).length > 0
73
+ );
74
+ }
75
+
76
+ export async function runTestPageGuards(
77
+ guards:
78
+ | PageGuard
79
+ | PageGuard[]
80
+ | PageGuardDefinition[],
81
+ options: TestPageGuardOptions = {}
82
+ ): Promise<PageGuardExecution> {
83
+ const list =
84
+ Array.isArray(guards)
85
+ ? guards
86
+ : [guards];
87
+ const definitions:
88
+ PageGuardDefinition[] =
89
+ list.map(
90
+ (candidate, index) => {
91
+ if (
92
+ typeof candidate === "object" &&
93
+ candidate !== null &&
94
+ "guard" in candidate
95
+ ) {
96
+ const definition =
97
+ candidate as PageGuardDefinition;
98
+ assertFunction(
99
+ definition.guard,
100
+ `page guard ${index + 1}`
101
+ );
102
+ return {
103
+ filePath:
104
+ definition.filePath ||
105
+ `${options.label ?? "bcp/testing guard"}#${index + 1}`,
106
+ guard:
107
+ definition.guard,
108
+ };
109
+ }
110
+
111
+ assertFunction(
112
+ candidate,
113
+ `page guard ${index + 1}`
114
+ );
115
+
116
+ return {
117
+ filePath:
118
+ `${options.label ?? "bcp/testing guard"}#${index + 1}`,
119
+ guard:
120
+ candidate as PageGuard,
121
+ };
122
+ }
123
+ );
124
+
125
+ return executePageGuardFunctions(
126
+ definitions,
127
+ {
128
+ ...(options.params ?? {}),
129
+ },
130
+ resolveTestPageUrl(
131
+ options.url
132
+ )
133
+ );
134
+ }
135
+
136
+ export async function runTestPageAction(
137
+ action: PageAction,
138
+ options: TestPageActionOptions = {}
139
+ ): Promise<PageActionExecution> {
140
+ assertFunction(
141
+ action,
142
+ "page action"
143
+ );
144
+
145
+ if (
146
+ options.formData &&
147
+ options.fields
148
+ ) {
149
+ throw new TypeError(
150
+ "BCP Testing: page action test cannot specify both formData and fields."
151
+ );
152
+ }
153
+
154
+ const actionName =
155
+ normalizeText(
156
+ options.actionName ?? "action",
157
+ "page action name"
158
+ );
159
+ const method =
160
+ normalizePageActionMethod(
161
+ options.method ?? "POST"
162
+ );
163
+ const formData =
164
+ options.formData ??
165
+ createTestFormData(
166
+ options.fields ?? {}
167
+ );
168
+
169
+ return executePageActionFunction(
170
+ action,
171
+ actionName,
172
+ method,
173
+ formData,
174
+ {
175
+ ...(options.params ?? {}),
176
+ },
177
+ resolveTestPageUrl(
178
+ options.url
179
+ ),
180
+ {
181
+ ...(options.guardData ?? {}),
182
+ },
183
+ options.hasGuard ??
184
+ Object.keys(
185
+ options.guardData ?? {}
186
+ ).length > 0,
187
+ options.label ??
188
+ "bcp/testing actions"
189
+ );
190
+ }
191
+
192
+ export function createTestFormData(
193
+ fields: Record<
194
+ string,
195
+ string | Blob | Array<string | Blob>
196
+ > = {}
197
+ ): FormData {
198
+ const formData =
199
+ new FormData();
200
+
201
+ for (
202
+ const [name, rawValue]
203
+ of Object.entries(fields)
204
+ ) {
205
+ const values =
206
+ Array.isArray(rawValue)
207
+ ? rawValue
208
+ : [rawValue];
209
+
210
+ for (const value of values) {
211
+ formData.append(
212
+ name,
213
+ value
214
+ );
215
+ }
216
+ }
217
+
218
+ return formData;
219
+ }
220
+
221
+ function resolveTestPageUrl(
222
+ value?: string | URL
223
+ ): URL {
224
+ if (value instanceof URL) {
225
+ return new URL(
226
+ value.href
227
+ );
228
+ }
229
+
230
+ const candidate =
231
+ value ??
232
+ "http://bcp.test/";
233
+
234
+ try {
235
+ return new URL(
236
+ candidate
237
+ );
238
+ } catch {
239
+ return new URL(
240
+ String(candidate).replace(
241
+ /^\//,
242
+ ""
243
+ ),
244
+ "http://bcp.test/"
245
+ );
246
+ }
247
+ }
248
+
249
+ function assertFunction(
250
+ value: unknown,
251
+ label: string
252
+ ): asserts value is (...args: any[]) => unknown {
253
+ if (typeof value !== "function") {
254
+ throw new TypeError(
255
+ `BCP Testing: ${label} must be a function.`
256
+ );
257
+ }
258
+ }
259
+
260
+ function normalizeText(
261
+ value: unknown,
262
+ label: string
263
+ ): string {
264
+ const text =
265
+ String(value ?? "").trim();
266
+
267
+ if (!text) {
268
+ throw new TypeError(
269
+ `BCP Testing: ${label} must be a non-empty string.`
270
+ );
271
+ }
272
+
273
+ return text;
274
+ }