@client-compose/client 0.5.1 → 0.7.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.
- package/README.md +8 -1
- package/dist/index.d.ts +39 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -21
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +9 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +87 -21
- package/dist/proxy.js.map +1 -1
- package/dist/testing.d.ts +19 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +118 -20
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +218 -20
- package/src/proxy.ts +107 -25
- package/src/testing.ts +147 -20
package/src/testing.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// const gateway = createFakeGateway({
|
|
6
6
|
// screens: { profile: () => screen("profile", [island("header", ProfileHeaderV1, { name: "Ada" })]) },
|
|
7
7
|
// actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
|
|
8
|
+
// islands: { "profile/posts": (r) => islandResponse("profile", island("posts", PostsV1, …, "page:3")) },
|
|
8
9
|
// });
|
|
9
10
|
// // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
|
|
10
11
|
//
|
|
@@ -46,6 +47,16 @@ export interface RecordedScreenRequest extends RecordedRequest {
|
|
|
46
47
|
islands: Capabilities;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** An island request (spec §4.5): a page of one island, or the island resolved again. */
|
|
51
|
+
export interface RecordedIslandRequest extends RecordedRequest {
|
|
52
|
+
screen: string;
|
|
53
|
+
key: string;
|
|
54
|
+
version: number;
|
|
55
|
+
params: Record<string, string>;
|
|
56
|
+
cursor: string | undefined;
|
|
57
|
+
client: ClientContext | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
export interface RecordedActionRequest extends RecordedRequest {
|
|
50
61
|
action: string;
|
|
51
62
|
version: number;
|
|
@@ -59,6 +70,8 @@ export interface FakeGatewayHandlers {
|
|
|
59
70
|
screens?: Record<string, (request: RecordedScreenRequest) => FakeResponse>;
|
|
60
71
|
/** By action name. */
|
|
61
72
|
actions?: Record<string, (request: RecordedActionRequest) => FakeResponse>;
|
|
73
|
+
/** Island requests, by `"{screen}/{key}"`. */
|
|
74
|
+
islands?: Record<string, (request: RecordedIslandRequest) => FakeResponse>;
|
|
62
75
|
/** Anything else, e.g. uploads to storage: return undefined to leave a request to the others. */
|
|
63
76
|
other?: (request: RecordedRequest) => FakeResponse | undefined;
|
|
64
77
|
}
|
|
@@ -69,6 +82,7 @@ export interface FakeGateway {
|
|
|
69
82
|
/** Every request received, in order. */
|
|
70
83
|
requests: RecordedRequest[];
|
|
71
84
|
screenRequests(): RecordedScreenRequest[];
|
|
85
|
+
islandRequests(): RecordedIslandRequest[];
|
|
72
86
|
actionRequests(): RecordedActionRequest[];
|
|
73
87
|
}
|
|
74
88
|
|
|
@@ -76,14 +90,18 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
76
90
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
91
|
|
|
78
92
|
function strings(value: unknown): Record<string, string> {
|
|
79
|
-
if (!isRecord(value))
|
|
93
|
+
if (!isRecord(value)) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
80
96
|
return Object.fromEntries(
|
|
81
97
|
Object.entries(value).filter((e): e is [string, string] => typeof e[1] === "string"),
|
|
82
98
|
);
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
function capabilities(value: unknown): Capabilities {
|
|
86
|
-
if (!isRecord(value))
|
|
102
|
+
if (!isRecord(value)) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
87
105
|
return Object.fromEntries(
|
|
88
106
|
Object.entries(value).map(([type, versions]) => [
|
|
89
107
|
type,
|
|
@@ -97,13 +115,20 @@ function clientContext(value: unknown): ClientContext | undefined {
|
|
|
97
115
|
!isRecord(value) ||
|
|
98
116
|
typeof value.platform !== "string" ||
|
|
99
117
|
typeof value.appVersion !== "string"
|
|
100
|
-
)
|
|
118
|
+
) {
|
|
101
119
|
return undefined;
|
|
102
|
-
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...value,
|
|
123
|
+
platform: value.platform,
|
|
124
|
+
appVersion: value.appVersion,
|
|
125
|
+
} as ClientContext;
|
|
103
126
|
}
|
|
104
127
|
|
|
105
128
|
function parseBody(body: unknown): unknown {
|
|
106
|
-
if (typeof body !== "string")
|
|
129
|
+
if (typeof body !== "string") {
|
|
130
|
+
return body;
|
|
131
|
+
}
|
|
107
132
|
try {
|
|
108
133
|
return JSON.parse(body) as unknown;
|
|
109
134
|
} catch {
|
|
@@ -112,7 +137,9 @@ function parseBody(body: unknown): unknown {
|
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
function urlOf(input: RequestInfo | URL): string {
|
|
115
|
-
if (typeof input === "string")
|
|
140
|
+
if (typeof input === "string") {
|
|
141
|
+
return input;
|
|
142
|
+
}
|
|
116
143
|
return input instanceof URL ? input.href : input.url;
|
|
117
144
|
}
|
|
118
145
|
|
|
@@ -122,10 +149,16 @@ function headersOf(init: HeadersInit | undefined): Record<string, string> {
|
|
|
122
149
|
return out;
|
|
123
150
|
}
|
|
124
151
|
|
|
152
|
+
function isIslandURL(url: string): boolean {
|
|
153
|
+
return /\/screens\/[^/]+\/islands\/[^/?]+/.test(url);
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
function asScreenRequest(request: RecordedRequest): RecordedScreenRequest | undefined {
|
|
126
157
|
const { body } = request;
|
|
127
|
-
|
|
158
|
+
const isScreen = request.url.includes("/screens/") && !isIslandURL(request.url);
|
|
159
|
+
if (!isScreen || !isRecord(body) || typeof body.screen !== "string") {
|
|
128
160
|
return undefined;
|
|
161
|
+
}
|
|
129
162
|
return {
|
|
130
163
|
...request,
|
|
131
164
|
screen: body.screen,
|
|
@@ -135,10 +168,30 @@ function asScreenRequest(request: RecordedRequest): RecordedScreenRequest | unde
|
|
|
135
168
|
};
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
function asIslandRequest(request: RecordedRequest): RecordedIslandRequest | undefined {
|
|
172
|
+
const { body } = request;
|
|
173
|
+
if (!isIslandURL(request.url) || !isRecord(body)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (typeof body.screen !== "string" || typeof body.key !== "string") {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...request,
|
|
181
|
+
screen: body.screen,
|
|
182
|
+
key: body.key,
|
|
183
|
+
version: typeof body.version === "number" ? body.version : 0,
|
|
184
|
+
params: strings(body.params),
|
|
185
|
+
cursor: typeof body.cursor === "string" ? body.cursor : undefined,
|
|
186
|
+
client: clientContext(body.client),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
function asActionRequest(request: RecordedRequest): RecordedActionRequest | undefined {
|
|
139
191
|
const { body } = request;
|
|
140
|
-
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string")
|
|
192
|
+
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string") {
|
|
141
193
|
return undefined;
|
|
194
|
+
}
|
|
142
195
|
return {
|
|
143
196
|
...request,
|
|
144
197
|
action: body.action,
|
|
@@ -169,13 +222,25 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
169
222
|
requests.push(request);
|
|
170
223
|
|
|
171
224
|
const other = handlers.other?.(request);
|
|
172
|
-
if (other)
|
|
225
|
+
if (other) {
|
|
226
|
+
return respond(other);
|
|
227
|
+
}
|
|
228
|
+
const islandRequest = asIslandRequest(request);
|
|
229
|
+
const islandHandler =
|
|
230
|
+
islandRequest && handlers.islands?.[`${islandRequest.screen}/${islandRequest.key}`];
|
|
231
|
+
if (islandRequest && islandHandler) {
|
|
232
|
+
return respond(islandHandler(islandRequest));
|
|
233
|
+
}
|
|
173
234
|
const screenRequest = asScreenRequest(request);
|
|
174
235
|
const screenHandler = screenRequest && handlers.screens?.[screenRequest.screen];
|
|
175
|
-
if (screenRequest && screenHandler)
|
|
236
|
+
if (screenRequest && screenHandler) {
|
|
237
|
+
return respond(screenHandler(screenRequest));
|
|
238
|
+
}
|
|
176
239
|
const actionRequest = asActionRequest(request);
|
|
177
240
|
const actionHandler = actionRequest && handlers.actions?.[actionRequest.action];
|
|
178
|
-
if (actionRequest && actionHandler)
|
|
241
|
+
if (actionRequest && actionHandler) {
|
|
242
|
+
return respond(actionHandler(actionRequest));
|
|
243
|
+
}
|
|
179
244
|
throw new Error(
|
|
180
245
|
`fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`,
|
|
181
246
|
);
|
|
@@ -184,6 +249,7 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
184
249
|
fetch: fakeFetch,
|
|
185
250
|
requests,
|
|
186
251
|
screenRequests: () => requests.flatMap((r) => asScreenRequest(r) ?? []),
|
|
252
|
+
islandRequests: () => requests.flatMap((r) => asIslandRequest(r) ?? []),
|
|
187
253
|
actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
|
|
188
254
|
};
|
|
189
255
|
}
|
|
@@ -211,9 +277,24 @@ export function installFakeGateway(handlers: FakeGatewayHandlers = {}): Installe
|
|
|
211
277
|
|
|
212
278
|
// ---- Responses ----
|
|
213
279
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
280
|
+
/**
|
|
281
|
+
* An island in a screen or island response, typed by its generated descriptor. Give `next` for an
|
|
282
|
+
* island with more pages (spec §4.5).
|
|
283
|
+
*/
|
|
284
|
+
export function island<P>(
|
|
285
|
+
key: string,
|
|
286
|
+
descriptor: IslandDescriptor<P>,
|
|
287
|
+
props: P,
|
|
288
|
+
next?: string,
|
|
289
|
+
): IslandEntry<P> {
|
|
290
|
+
return {
|
|
291
|
+
key,
|
|
292
|
+
type: descriptor.type,
|
|
293
|
+
version: descriptor.version,
|
|
294
|
+
status: "ok",
|
|
295
|
+
props,
|
|
296
|
+
...(next !== undefined && { next }),
|
|
297
|
+
};
|
|
217
298
|
}
|
|
218
299
|
|
|
219
300
|
/** An island that failed on the composer (spec §3.4). */
|
|
@@ -222,7 +303,13 @@ export function islandError(
|
|
|
222
303
|
descriptor: IslandDescriptor<unknown>,
|
|
223
304
|
error: IslandError,
|
|
224
305
|
): IslandEntry {
|
|
225
|
-
return {
|
|
306
|
+
return {
|
|
307
|
+
key,
|
|
308
|
+
type: descriptor.type,
|
|
309
|
+
version: descriptor.version,
|
|
310
|
+
status: "error",
|
|
311
|
+
error,
|
|
312
|
+
};
|
|
226
313
|
}
|
|
227
314
|
|
|
228
315
|
/** A screen response. */
|
|
@@ -233,7 +320,24 @@ export function screen(
|
|
|
233
320
|
): FakeResponse {
|
|
234
321
|
return {
|
|
235
322
|
status: 200,
|
|
236
|
-
body: {
|
|
323
|
+
body: {
|
|
324
|
+
protocol: PROTOCOL_VERSION,
|
|
325
|
+
screen: id,
|
|
326
|
+
islands,
|
|
327
|
+
...(meta && { meta }),
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** An island response (spec §4.5): a page of one island, or the island resolved again. */
|
|
333
|
+
export function islandResponse(screen: string, entry: IslandEntry<unknown>): FakeResponse {
|
|
334
|
+
return {
|
|
335
|
+
status: 200,
|
|
336
|
+
body: {
|
|
337
|
+
protocol: PROTOCOL_VERSION,
|
|
338
|
+
screen,
|
|
339
|
+
island: entry,
|
|
340
|
+
},
|
|
237
341
|
};
|
|
238
342
|
}
|
|
239
343
|
|
|
@@ -248,8 +352,20 @@ export function actionOk<I, R, E extends DeclaredError>(
|
|
|
248
352
|
result: R,
|
|
249
353
|
effects?: ActionEffects,
|
|
250
354
|
): FakeResponse {
|
|
251
|
-
const base = {
|
|
252
|
-
|
|
355
|
+
const base = {
|
|
356
|
+
protocol: PROTOCOL_VERSION,
|
|
357
|
+
action: action.action,
|
|
358
|
+
version: action.version,
|
|
359
|
+
};
|
|
360
|
+
return {
|
|
361
|
+
status: 200,
|
|
362
|
+
body: {
|
|
363
|
+
...base,
|
|
364
|
+
status: "ok",
|
|
365
|
+
result,
|
|
366
|
+
...(effects && { effects }),
|
|
367
|
+
},
|
|
368
|
+
};
|
|
253
369
|
}
|
|
254
370
|
|
|
255
371
|
/** An action that ended with one of its declared errors. */
|
|
@@ -257,8 +373,19 @@ export function actionError<I, R, E extends DeclaredError>(
|
|
|
257
373
|
action: ActionDescriptor<I, R, E>,
|
|
258
374
|
error: E,
|
|
259
375
|
): FakeResponse {
|
|
260
|
-
const base = {
|
|
261
|
-
|
|
376
|
+
const base = {
|
|
377
|
+
protocol: PROTOCOL_VERSION,
|
|
378
|
+
action: action.action,
|
|
379
|
+
version: action.version,
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
status: 200,
|
|
383
|
+
body: {
|
|
384
|
+
...base,
|
|
385
|
+
status: "error",
|
|
386
|
+
error,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
262
389
|
}
|
|
263
390
|
|
|
264
391
|
/** An action the composer didn't perform, e.g. `actionFailure(401, "unauthenticated")` (spec §5.4). */
|