@remit/web-client 0.0.276 → 0.0.277

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.276",
3
+ "version": "0.0.277",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -0,0 +1,105 @@
1
+ // biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
2
+ /**
3
+ * Issue #615: the remove control in Settings > Senders sent `{ flags: {
4
+ * [group]: null } }`. The server's openapi-backend validator doesn't honour
5
+ * `nullable: true` on the `allOf`-wrapped flag schema TypeSpec emits for
6
+ * `TrustedFlag | null`, so it rejects the null form as invalid — the same gap
7
+ * `useToggleTrusted` already works around by sending `{ value: false }`
8
+ * instead of `null`. This mock server enforces that same rejection, so the
9
+ * pre-fix `null` body surfaces as a failed mutation (the row never clears)
10
+ * rather than a silently accepted no-op.
11
+ */
12
+
13
+ import assert from "node:assert/strict";
14
+ import { afterEach, describe, it } from "node:test";
15
+ import type { RemitImapAddressResponse } from "@remit/api-http-client/types.gen.ts";
16
+ import { createElement } from "react";
17
+ import { createDomHarness, type DomHarness } from "@/test-support/dom";
18
+ import {
19
+ type HttpCall,
20
+ type HttpMock,
21
+ httpError,
22
+ mockFetch,
23
+ } from "@/test-support/http";
24
+ import { SearchGroupPane } from "./senders";
25
+
26
+ const ADDRESS_ID = "addr-1";
27
+
28
+ let harness: DomHarness | undefined;
29
+ let http: HttpMock | undefined;
30
+ let mutedFlag: { value: boolean; setAt: number } | undefined;
31
+
32
+ afterEach(() => {
33
+ harness?.close();
34
+ harness = undefined;
35
+ http?.restore();
36
+ http = undefined;
37
+ mutedFlag = undefined;
38
+ });
39
+
40
+ const address = (): RemitImapAddressResponse =>
41
+ ({
42
+ addressId: ADDRESS_ID,
43
+ displayName: "Alex Rivera",
44
+ normalizedEmail: "alex@example.com",
45
+ flags: mutedFlag ? { muted: mutedFlag } : {},
46
+ }) as RemitImapAddressResponse;
47
+
48
+ const readFlagsPatch = (call: HttpCall): unknown => {
49
+ const body = call.body as { flags?: Record<string, unknown> } | undefined;
50
+ return body?.flags?.muted;
51
+ };
52
+
53
+ const respond = (call: HttpCall): unknown => {
54
+ if (call.method === "GET" && call.path === "/addresses/search") {
55
+ return { items: [address()] };
56
+ }
57
+ if (call.method === "PATCH" && call.path === `/addresses/${ADDRESS_ID}`) {
58
+ const patch = readFlagsPatch(call);
59
+ if (patch === null) {
60
+ return httpError(400, "flags.muted: nullable removal is not accepted");
61
+ }
62
+ mutedFlag = patch as { value: boolean; setAt: number };
63
+ return address();
64
+ }
65
+ return {};
66
+ };
67
+
68
+ const mount = async (): Promise<void> => {
69
+ mutedFlag = { value: true, setAt: 1_700_000_000_000 };
70
+ http = mockFetch(respond);
71
+ harness = createDomHarness();
72
+ harness.renderApp(
73
+ createElement(SearchGroupPane, { group: "muted", query: "alex" }),
74
+ );
75
+ await harness.waitFor(
76
+ () => harness?.text().includes("alex@example.com") ?? false,
77
+ );
78
+ };
79
+
80
+ describe("removing a sender flag (#615)", () => {
81
+ it("sends a request body the server accepts, and the flag clears from the list", async () => {
82
+ await mount();
83
+ if (!harness) throw new Error("nothing mounted");
84
+
85
+ harness.click(harness.byLabel("Remove muted flag"));
86
+
87
+ await harness.waitFor(
88
+ () => !(harness?.text().includes("alex@example.com") ?? true),
89
+ "the unmuted sender to drop out of the muted list",
90
+ );
91
+
92
+ const patches = (http?.calls ?? []).filter(
93
+ (call) =>
94
+ call.method === "PATCH" && call.path === `/addresses/${ADDRESS_ID}`,
95
+ );
96
+ assert.equal(patches.length, 1);
97
+ const sentFlag = readFlagsPatch(patches[0]) as
98
+ | { value: boolean; setAt: number }
99
+ | null
100
+ | undefined;
101
+ assert.notEqual(sentFlag, null, "the server rejects a null flag removal");
102
+ assert.equal(sentFlag?.value, false);
103
+ assert.equal(typeof sentFlag?.setAt, "number");
104
+ });
105
+ });
@@ -174,7 +174,7 @@ function VipGroupPane({
174
174
  /* Muted / Blocked groups: search-based (backend gap noted) */
175
175
  /* ------------------------------------------------------------------ */
176
176
 
177
- function SearchGroupPane({
177
+ export function SearchGroupPane({
178
178
  group,
179
179
  query,
180
180
  onCount,
@@ -303,7 +303,9 @@ function SearchGroupPane({
303
303
  setRetryId(a.addressId);
304
304
  removeMutation.mutate({
305
305
  path: { addressId: a.addressId },
306
- body: { flags: { [group]: null } },
306
+ body: {
307
+ flags: { [group]: { value: false, setAt: Date.now() } },
308
+ },
307
309
  });
308
310
  }}
309
311
  >
@@ -321,7 +323,9 @@ function SearchGroupPane({
321
323
  setRetryId(a.addressId);
322
324
  removeMutation.mutate({
323
325
  path: { addressId: a.addressId },
324
- body: { flags: { [group]: null } },
326
+ body: {
327
+ flags: { [group]: { value: false, setAt: Date.now() } },
328
+ },
325
329
  });
326
330
  }}
327
331
  />