@client-compose/client 0.8.0 → 0.9.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/src/testing.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // screens: { profile: () => screen("profile", [island("header", ProfileHeaderV1, { name: "Ada" })]) },
7
7
  // actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
8
8
  // islands: { "profile/posts": (r) => islandResponse("profile", island("posts", PostsV1, …, "page:3")) },
9
+ // strings: () => stringsResponse("en", { retry: "Try again" }),
9
10
  // });
10
11
  // // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
11
12
  //
@@ -21,8 +22,10 @@ import {
21
22
  type IslandEntry,
22
23
  type IslandError,
23
24
  type PendingIsland,
25
+ type PluralRules,
24
26
  PROTOCOL_VERSION,
25
27
  type ScreenMeta,
28
+ type Strings,
26
29
  } from "@client-compose/protocol";
27
30
 
28
31
  /**
@@ -71,6 +74,11 @@ export interface RecordedActionRequest extends RecordedRequest {
71
74
  idempotencyKey: string | undefined;
72
75
  }
73
76
 
77
+ /** A strings request (spec §4.7): the app strings, in the client's language. */
78
+ export interface RecordedStringsRequest extends RecordedRequest {
79
+ client: ClientContext | undefined;
80
+ }
81
+
74
82
  export interface FakeGatewayHandlers {
75
83
  /** By screen id. */
76
84
  screens?: Record<string, (request: RecordedScreenRequest) => FakeResponse>;
@@ -78,6 +86,8 @@ export interface FakeGatewayHandlers {
78
86
  actions?: Record<string, (request: RecordedActionRequest) => FakeResponse>;
79
87
  /** Island requests, by `"{screen}/{key}"`. */
80
88
  islands?: Record<string, (request: RecordedIslandRequest) => FakeResponse>;
89
+ /** The app strings (POST /strings). */
90
+ strings?: (request: RecordedStringsRequest) => FakeResponse;
81
91
  /** Anything else, e.g. uploads to storage: return undefined to leave a request to the others. */
82
92
  other?: (request: RecordedRequest) => FakeResponse | undefined;
83
93
  }
@@ -90,6 +100,7 @@ export interface FakeGateway {
90
100
  screenRequests(): RecordedScreenRequest[];
91
101
  islandRequests(): RecordedIslandRequest[];
92
102
  actionRequests(): RecordedActionRequest[];
103
+ stringsRequests(): RecordedStringsRequest[];
93
104
  }
94
105
 
95
106
  const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -208,6 +219,14 @@ function asActionRequest(request: RecordedRequest): RecordedActionRequest | unde
208
219
  };
209
220
  }
210
221
 
222
+ function asStringsRequest(request: RecordedRequest): RecordedStringsRequest | undefined {
223
+ const path = request.url.split("?")[0] ?? "";
224
+ if (!/\/strings\/?$/.test(path) || !isRecord(request.body)) {
225
+ return undefined;
226
+ }
227
+ return { ...request, client: clientContext(request.body.client) };
228
+ }
229
+
211
230
  function respond(response: FakeResponse): Response {
212
231
  if (response.stream) {
213
232
  return new Response(response.stream(), {
@@ -253,6 +272,10 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
253
272
  if (actionRequest && actionHandler) {
254
273
  return respond(actionHandler(actionRequest));
255
274
  }
275
+ const stringsRequest = asStringsRequest(request);
276
+ if (stringsRequest && handlers.strings) {
277
+ return respond(handlers.strings(stringsRequest));
278
+ }
256
279
  throw new Error(
257
280
  `fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`,
258
281
  );
@@ -263,6 +286,7 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
263
286
  screenRequests: () => requests.flatMap((r) => asScreenRequest(r) ?? []),
264
287
  islandRequests: () => requests.flatMap((r) => asIslandRequest(r) ?? []),
265
288
  actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
289
+ stringsRequests: () => requests.flatMap((r) => asStringsRequest(r) ?? []),
266
290
  };
267
291
  }
268
292
 
@@ -289,16 +313,24 @@ export function installFakeGateway(handlers: FakeGatewayHandlers = {}): Installe
289
313
 
290
314
  // ---- Responses ----
291
315
 
316
+ /** More of an island's entry: its next page's cursor (spec §4.5), and its strings (spec §4.7). */
317
+ export interface IslandOptions {
318
+ next?: string;
319
+ strings?: Strings;
320
+ }
321
+
292
322
  /**
293
323
  * An island in a screen or island response, typed by its generated descriptor. Give `next` for an
294
- * island with more pages (spec §4.5).
324
+ * island with more pages (spec §4.5), or `{ next, strings }`.
295
325
  */
296
- export function island<P>(
326
+ export function island<P, S>(
297
327
  key: string,
298
- descriptor: IslandDescriptor<P>,
328
+ descriptor: IslandDescriptor<P, S>,
299
329
  props: P,
300
- next?: string,
330
+ more?: string | IslandOptions,
301
331
  ): IslandEntry<P> {
332
+ const { next, strings } =
333
+ typeof more === "string" ? { next: more, strings: undefined } : (more ?? {});
302
334
  return {
303
335
  key,
304
336
  type: descriptor.type,
@@ -306,9 +338,16 @@ export function island<P>(
306
338
  status: "ok",
307
339
  props,
308
340
  ...(next !== undefined && { next }),
341
+ ...(strings !== undefined && { strings }),
309
342
  };
310
343
  }
311
344
 
345
+ /** The language a screen is in, and its plural rules (spec §4.7). */
346
+ export interface ScreenLanguage {
347
+ locale: string;
348
+ plurals: PluralRules;
349
+ }
350
+
312
351
  /** An island that failed on the composer (spec §3.4). */
313
352
  export function islandError(
314
353
  key: string,
@@ -329,6 +368,7 @@ export function screen(
329
368
  id: string,
330
369
  islands: IslandEntry<unknown>[],
331
370
  meta?: ScreenMeta,
371
+ language?: ScreenLanguage,
332
372
  ): FakeResponse {
333
373
  const head = {
334
374
  kind: "head",
@@ -336,6 +376,7 @@ export function screen(
336
376
  screen: id,
337
377
  islands,
338
378
  ...(meta && { meta }),
379
+ ...language,
339
380
  };
340
381
  return {
341
382
  status: 200,
@@ -376,6 +417,7 @@ export function streamedScreen(
376
417
  id: string,
377
418
  islands: (IslandEntry<unknown> | PendingIsland)[],
378
419
  meta?: ScreenMeta,
420
+ language?: ScreenLanguage,
379
421
  ): StreamedScreen {
380
422
  const encoder = new TextEncoder();
381
423
  const queued: string[] = [];
@@ -399,6 +441,7 @@ export function streamedScreen(
399
441
  screen: id,
400
442
  islands,
401
443
  ...(meta && { meta }),
444
+ ...language,
402
445
  });
403
446
  return {
404
447
  status: 200,
@@ -447,6 +490,23 @@ export function islandResponse(screen: string, entry: IslandEntry<unknown>): Fak
447
490
  };
448
491
  }
449
492
 
493
+ /** The app strings in a language (spec §4.7), with its plural rules if its counted strings need them. */
494
+ export function stringsResponse(
495
+ locale: string,
496
+ strings: Strings,
497
+ plurals: PluralRules = {},
498
+ ): FakeResponse {
499
+ return {
500
+ status: 200,
501
+ body: {
502
+ protocol: PROTOCOL_VERSION,
503
+ locale,
504
+ plurals,
505
+ strings,
506
+ },
507
+ };
508
+ }
509
+
450
510
  /** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
451
511
  export function screenError(status: number, code: string): FakeResponse {
452
512
  return { status, body: { protocol: PROTOCOL_VERSION, error: { code } } };