@remit/web-client 0.0.155 → 0.0.156

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.155",
3
+ "version": "0.0.156",
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,114 @@
1
+ /**
2
+ * The failure path of #707, end to end: a checker that will not start has to
3
+ * say so on screen. Each link in that chain is covered on its own elsewhere —
4
+ * what is asserted here is that they are actually joined, because every break
5
+ * in it looks identical from the writer's seat. A composer with no squiggles
6
+ * reads as a message with nothing wrong in it, and that is the one thing it
7
+ * must never come to mean by accident.
8
+ */
9
+
10
+ import assert from "node:assert/strict";
11
+ import { after, before, describe, it } from "node:test";
12
+ import type { ComposeBody as ComposeBodyType } from "@remit/ui/rich-text";
13
+ import { createElement, useMemo } from "react";
14
+ import { createDomHarness, type DomHarness } from "../../test-support/dom";
15
+ import { useErrorBanners } from "../ui/ErrorBannerProvider";
16
+ import { composeSpellcheck } from "./compose-spellcheck.js";
17
+
18
+ let harness: DomHarness | undefined;
19
+ let ComposeBody: typeof ComposeBodyType;
20
+
21
+ class Marks {
22
+ readonly ranges: Range[] = [];
23
+ add(range: Range): void {
24
+ this.ranges.push(range);
25
+ }
26
+ }
27
+
28
+ const FAILURE = "the worker could not be started";
29
+
30
+ before(async () => {
31
+ // The marks are drawn through the CSS Custom Highlight registry, which jsdom
32
+ // has neither half of. Without both the editor draws nothing and opens no
33
+ // provider at all, so there would be no failure to observe.
34
+ Object.defineProperty(globalThis, "CSS", {
35
+ value: { highlights: new Map<string, Marks>() },
36
+ configurable: true,
37
+ });
38
+ Object.defineProperty(globalThis, "Highlight", {
39
+ value: Marks,
40
+ configurable: true,
41
+ });
42
+ harness = createDomHarness();
43
+ // remit-ui's `.tsx` is transpiled here with the classic JSX runtime, which
44
+ // reads a global `React`. The harness installs it, so the editor is pulled
45
+ // in after one exists rather than at import time.
46
+ ({ ComposeBody } = await import("@remit/ui/rich-text"));
47
+ });
48
+
49
+ after(() => {
50
+ harness?.close();
51
+ harness = undefined;
52
+ });
53
+
54
+ /**
55
+ * The composer as `ComposeForm` wires it — the app's own options object, with
56
+ * only the module load behind the provider swapped for one that fails the way
57
+ * a missing chunk does.
58
+ */
59
+ const Composer = () => {
60
+ const { pushError } = useErrorBanners();
61
+ const spellcheck = useMemo(
62
+ () => ({
63
+ ...composeSpellcheck(pushError),
64
+ provider: () => Promise.reject(new Error(FAILURE)),
65
+ }),
66
+ [pushError],
67
+ );
68
+ return createElement(ComposeBody, {
69
+ mode: "rich",
70
+ onModeChange: () => undefined,
71
+ initialHtml: "<p>Ths is redy.</p>",
72
+ initialText: "Ths is redy.",
73
+ onChange: () => undefined,
74
+ onConversionError: () => undefined,
75
+ onLanguageChange: () => undefined,
76
+ languages: ["en"],
77
+ spellcheck,
78
+ });
79
+ };
80
+
81
+ const mounted = (): DomHarness => {
82
+ if (!harness) throw new Error("the harness is not mounted");
83
+ return harness;
84
+ };
85
+
86
+ describe("a checker the composer cannot start", () => {
87
+ it("says what stopped, offers the report, and gives the message back", async () => {
88
+ mounted().renderApp(createElement(Composer));
89
+ await mounted().flush();
90
+
91
+ const banner = mounted().query('[aria-label="Notifications"]');
92
+ assert.ok(
93
+ banner,
94
+ "a checker that failed to start is on screen, not silent",
95
+ );
96
+ assert.match(banner.textContent ?? "", /Spellcheck stopped/);
97
+ assert.match(banner.textContent ?? "", /Spellcheck for en is off/);
98
+ assert.match(banner.textContent ?? "", /checker stopped running/);
99
+
100
+ const report = banner.querySelector<HTMLAnchorElement>("a[href]");
101
+ assert.ok(report, "the failure carries a way to report it");
102
+ const href = new URL(report.href);
103
+ assert.match(href.href, /github\.com\/.+\/issues\/new\?/);
104
+ assert.match(href.searchParams.get("body") ?? "", new RegExp(FAILURE));
105
+
106
+ const editable = mounted().query("[data-testid=compose-body]");
107
+ assert.ok(editable, "the writing surface is mounted");
108
+ assert.equal(
109
+ editable.getAttribute("spellcheck"),
110
+ "true",
111
+ "the browser checks the message when ours cannot",
112
+ );
113
+ });
114
+ });
@@ -0,0 +1,86 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { ErrorBanner } from "./ErrorBanner";
3
+
4
+ /**
5
+ * The soft, dismissible notification (#55). It sits over the toolbar and the
6
+ * message list, so it is opaque and it names its own severity out loud rather
7
+ * than leaving colour to carry the meaning.
8
+ *
9
+ * A failure the user cannot act on still gets an action link, prefilled, so
10
+ * reporting it is one click instead of a form they have to assemble. The
11
+ * hrefs below stand in for the real report URL, which the app builds from
12
+ * build-time constants Storybook has no `define` for.
13
+ */
14
+ const meta: Meta<typeof ErrorBanner> = {
15
+ title: "Components/ErrorBanner",
16
+ component: ErrorBanner,
17
+ parameters: { layout: "padded" },
18
+ args: {
19
+ id: "banner-1",
20
+ onDismiss: () => undefined,
21
+ },
22
+ };
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof ErrorBanner>;
26
+
27
+ const REPORT_URL =
28
+ "https://github.com/remit-mail/reader/issues/new?title=Spellcheck+stopped";
29
+
30
+ /** A mutation that failed, with the reason underneath. */
31
+ export const Failed: Story = {
32
+ name: "Error",
33
+ args: {
34
+ severity: "error",
35
+ title: "Couldn't move message",
36
+ detail: "Connection reset by peer",
37
+ },
38
+ };
39
+
40
+ /** Nothing more to say than the title. */
41
+ export const NoDetail: Story = {
42
+ args: {
43
+ severity: "error",
44
+ title: "Couldn't move message",
45
+ },
46
+ };
47
+
48
+ /** Something degraded rather than broke. */
49
+ export const Warning: Story = {
50
+ args: {
51
+ severity: "warning",
52
+ title: "Draft saved locally",
53
+ detail: "The server did not answer, so this draft has not been uploaded.",
54
+ },
55
+ };
56
+
57
+ /** A statement of fact, not a problem. */
58
+ export const Info: Story = {
59
+ args: {
60
+ severity: "info",
61
+ title: "Sync finished",
62
+ detail: "1,204 messages are up to date.",
63
+ },
64
+ };
65
+
66
+ /**
67
+ * The spellchecker stopping (#707): the writer cannot fix this one, so the
68
+ * banner says what stopped, what is happening instead, and offers the report
69
+ * already filled in.
70
+ */
71
+ export const WithActionLink: Story = {
72
+ args: {
73
+ severity: "warning",
74
+ title: "Spellcheck stopped",
75
+ detail:
76
+ "Spellcheck for en is off: the checker could not start. Your browser is checking this message instead. Failed to fetch dynamically imported module.",
77
+ action: { label: "Report this", href: REPORT_URL },
78
+ },
79
+ };
80
+
81
+ /** The action link on the dark theme. */
82
+ export const WithActionLinkDark: Story = {
83
+ name: "With Action Link (dark)",
84
+ parameters: { theme: "dark" },
85
+ args: WithActionLink.args,
86
+ };