@stigmer/react 3.1.3 → 3.1.4
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/composer/SessionComposer.d.ts +31 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +12 -4
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactApplyButton.d.ts +25 -0
- package/execution/ArtifactApplyButton.d.ts.map +1 -0
- package/execution/ArtifactApplyButton.js +21 -0
- package/execution/ArtifactApplyButton.js.map +1 -0
- package/execution/ArtifactContentBody.d.ts +36 -0
- package/execution/ArtifactContentBody.d.ts.map +1 -0
- package/execution/ArtifactContentBody.js +62 -0
- package/execution/ArtifactContentBody.js.map +1 -0
- package/execution/ArtifactDocument.d.ts +48 -0
- package/execution/ArtifactDocument.d.ts.map +1 -0
- package/execution/ArtifactDocument.js +89 -0
- package/execution/ArtifactDocument.js.map +1 -0
- package/execution/ArtifactPreviewModal.d.ts +4 -4
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +15 -142
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactRow.d.ts +49 -0
- package/execution/ArtifactRow.d.ts.map +1 -0
- package/execution/ArtifactRow.js +56 -0
- package/execution/ArtifactRow.js.map +1 -0
- package/execution/ArtifactsWidget.d.ts +6 -9
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +15 -19
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/index.d.ts +8 -2
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +4 -1
- package/execution/index.js.map +1 -1
- package/execution/useArtifactContent.d.ts +6 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +7 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useArtifactInspection.d.ts +85 -0
- package/execution/useArtifactInspection.d.ts.map +1 -0
- package/execution/useArtifactInspection.js +143 -0
- package/execution/useArtifactInspection.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +11 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +1 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/internal/MermaidDiagram.d.ts +47 -0
- package/internal/MermaidDiagram.d.ts.map +1 -0
- package/internal/MermaidDiagram.js +118 -0
- package/internal/MermaidDiagram.js.map +1 -0
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +34 -0
- package/internal/markdown-components.js.map +1 -1
- package/internal/mermaid-loader.d.ts +26 -0
- package/internal/mermaid-loader.d.ts.map +1 -0
- package/internal/mermaid-loader.js +34 -0
- package/internal/mermaid-loader.js.map +1 -0
- package/internal/mermaid-theme-css.d.ts +41 -0
- package/internal/mermaid-theme-css.d.ts.map +1 -0
- package/internal/mermaid-theme-css.js +179 -0
- package/internal/mermaid-theme-css.js.map +1 -0
- package/internal/slug.d.ts +9 -5
- package/internal/slug.d.ts.map +1 -1
- package/internal/slug.js +10 -6
- package/internal/slug.js.map +1 -1
- package/package.json +5 -4
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +91 -18
- package/session/SessionViewer.js.map +1 -1
- package/session/artifact-document.d.ts +13 -0
- package/session/artifact-document.d.ts.map +1 -0
- package/session/artifact-document.js +22 -0
- package/session/artifact-document.js.map +1 -0
- package/session/facets/ArtifactsTab.d.ts +30 -4
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +32 -10
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/index.d.ts +2 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +4 -1
- package/session/index.js.map +1 -1
- package/session/useSessionArtifacts.d.ts +17 -0
- package/session/useSessionArtifacts.d.ts.map +1 -1
- package/session/useSessionArtifacts.js +16 -5
- package/session/useSessionArtifacts.js.map +1 -1
- package/session/useSessionConversation.d.ts +17 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +32 -4
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionPanel.d.ts +20 -0
- package/session/useSessionPanel.d.ts.map +1 -1
- package/session/useSessionPanel.js +13 -0
- package/session/useSessionPanel.js.map +1 -1
- package/session/useSessionRailViews.d.ts +13 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +5 -3
- package/session/useSessionRailViews.js.map +1 -1
- package/src/composer/SessionComposer.tsx +69 -1
- package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
- package/src/execution/ArtifactApplyButton.tsx +79 -0
- package/src/execution/ArtifactContentBody.tsx +255 -0
- package/src/execution/ArtifactDocument.tsx +344 -0
- package/src/execution/ArtifactPreviewModal.tsx +39 -405
- package/src/execution/ArtifactRow.tsx +165 -0
- package/src/execution/ArtifactsWidget.tsx +24 -33
- package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
- package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
- package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
- package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
- package/src/execution/__tests__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/index.ts +14 -2
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/index.ts +11 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
- package/src/internal/__tests__/markdown-components.test.tsx +93 -1
- package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
- package/src/internal/markdown-components.tsx +36 -1
- package/src/internal/mermaid-loader.ts +44 -0
- package/src/internal/mermaid-theme-css.ts +178 -0
- package/src/internal/slug.ts +10 -6
- package/src/session/SessionViewer.tsx +177 -33
- package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
- package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
- package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
- package/src/session/__tests__/useSessionPanel.test.ts +70 -1
- package/src/session/artifact-document.ts +23 -0
- package/src/session/facets/ArtifactsTab.tsx +66 -18
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
- package/src/session/index.ts +5 -1
- package/src/session/useSessionArtifacts.ts +16 -5
- package/src/session/useSessionConversation.ts +48 -6
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/session/useSessionPanel.ts +41 -0
- package/src/session/useSessionRailViews.tsx +21 -1
- package/styles.css +1 -1
- package/execution/ArtifactCard.d.ts +0 -81
- package/execution/ArtifactCard.d.ts.map +0 -1
- package/execution/ArtifactCard.js +0 -130
- package/execution/ArtifactCard.js.map +0 -1
- package/src/execution/ArtifactCard.tsx +0 -320
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useIsCodeFenceIncomplete } from "streamdown";
|
|
5
|
+
import { useColorMode } from "../color-mode.js";
|
|
6
|
+
import { loadMermaid } from "./mermaid-loader.js";
|
|
7
|
+
import { MERMAID_THEME_CSS } from "./mermaid-theme-css.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `mermaid.render` requires a DOM id for its scratch element. Ids must be
|
|
11
|
+
* unique per render call — theme switches and streaming updates can overlap
|
|
12
|
+
* renders within one component — so a module counter is the simplest
|
|
13
|
+
* collision-free source.
|
|
14
|
+
*/
|
|
15
|
+
let renderSequence = 0;
|
|
16
|
+
|
|
17
|
+
/** Props for {@link MermaidDiagram}. */
|
|
18
|
+
export interface MermaidDiagramProps {
|
|
19
|
+
/** The raw Mermaid source from the fenced code block. */
|
|
20
|
+
readonly chart: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type RenderState =
|
|
24
|
+
| { readonly status: "pending" }
|
|
25
|
+
| { readonly status: "rendered"; readonly svg: string }
|
|
26
|
+
| { readonly status: "failed"; readonly message: string };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Renders a fenced ```mermaid block as an inline SVG diagram.
|
|
30
|
+
*
|
|
31
|
+
* Mounted by the shared `pre` override in
|
|
32
|
+
* {@link file://./markdown-components.tsx} whenever a fenced block carries the
|
|
33
|
+
* explicit `mermaid` info string — which means every SDK markdown surface
|
|
34
|
+
* (chat via Streamdown, artifacts/skills via react-markdown) renders diagrams
|
|
35
|
+
* identically. Detection is deliberately limited to the explicit language tag;
|
|
36
|
+
* guessing diagram intent from fence bodies (e.g. a bare fence starting with
|
|
37
|
+
* `flowchart`) is the kind of fuzzy heuristic this codebase avoids.
|
|
38
|
+
*
|
|
39
|
+
* Every failure mode degrades to the exact code block the fence rendered as
|
|
40
|
+
* before this component existed:
|
|
41
|
+
*
|
|
42
|
+
* - **Streaming** — while the fence is still open (`useIsCodeFenceIncomplete`,
|
|
43
|
+
* a safe no-op under react-markdown where the context defaults to `false`),
|
|
44
|
+
* the source renders as a code block; the diagram appears once the fence
|
|
45
|
+
* closes. No half-parsed error flashing mid-stream.
|
|
46
|
+
* - **Library unavailable** — `mermaid` is an optional peer dependency; if the
|
|
47
|
+
* dynamic import fails the source stays a code block.
|
|
48
|
+
* - **Parse error** — invalid diagram source falls back to the code block plus
|
|
49
|
+
* a screen-reader-announced note, so the agent's output is never hidden.
|
|
50
|
+
*
|
|
51
|
+
* Security: agent output is untrusted and this SDK renders inside host
|
|
52
|
+
* applications, so mermaid runs with `securityLevel: "strict"` (sanitized
|
|
53
|
+
* labels, no `click` interactivity) — deliberately stricter than the
|
|
54
|
+
* docs-site renderer, which only ever shows first-party content.
|
|
55
|
+
*
|
|
56
|
+
* Theming: the container chrome is fully `--stgm-*` token-driven, and so is
|
|
57
|
+
* the SVG interior. The built-in `default`/`dark` theme (selected via
|
|
58
|
+
* {@link useColorMode}) is the base; on top of it we inject
|
|
59
|
+
* {@link MERMAID_THEME_CSS}, a token-driven stylesheet whose `var(--stgm-*)`
|
|
60
|
+
* references the browser resolves against the `.stgm` scope. Interiors
|
|
61
|
+
* therefore track the active preset *and* color mode live — no color is read
|
|
62
|
+
* in JS, and a preset switch needs no re-render. Feeding tokens through
|
|
63
|
+
* mermaid's `themeVariables` is not possible: they are authored in `oklch()`,
|
|
64
|
+
* which mermaid's color engine (khroma) cannot parse. Any element or diagram
|
|
65
|
+
* type the CSS does not target falls back to the built-in theme, so a diagram
|
|
66
|
+
* is never blank or mis-colored.
|
|
67
|
+
*/
|
|
68
|
+
export function MermaidDiagram({ chart }: MermaidDiagramProps) {
|
|
69
|
+
const isIncomplete = useIsCodeFenceIncomplete();
|
|
70
|
+
const colorMode = useColorMode();
|
|
71
|
+
const [state, setState] = useState<RenderState>({ status: "pending" });
|
|
72
|
+
|
|
73
|
+
// Markdown parsers hand the fence body over with its trailing newline;
|
|
74
|
+
// mermaid treats leading/trailing blank lines as part of the diagram.
|
|
75
|
+
const source = chart.trim();
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (isIncomplete || source.length === 0) return;
|
|
79
|
+
|
|
80
|
+
let cancelled = false;
|
|
81
|
+
|
|
82
|
+
void (async () => {
|
|
83
|
+
const renderId = `stgm-mermaid-${++renderSequence}`;
|
|
84
|
+
try {
|
|
85
|
+
const mermaid = await loadMermaid();
|
|
86
|
+
// initialize() mutates a global singleton, so it runs before every
|
|
87
|
+
// render call — all diagrams under one provider share a color mode,
|
|
88
|
+
// and re-initializing keeps a theme switch from bleeding stale config
|
|
89
|
+
// into the next render.
|
|
90
|
+
mermaid.initialize({
|
|
91
|
+
startOnLoad: false,
|
|
92
|
+
securityLevel: "strict",
|
|
93
|
+
theme: colorMode === "dark" ? "dark" : "default",
|
|
94
|
+
themeCSS: MERMAID_THEME_CSS,
|
|
95
|
+
fontFamily: "inherit",
|
|
96
|
+
});
|
|
97
|
+
const { svg } = await mermaid.render(renderId, source);
|
|
98
|
+
if (!cancelled) setState({ status: "rendered", svg });
|
|
99
|
+
} catch (error: unknown) {
|
|
100
|
+
// Mermaid has historically leaked its scratch element into the
|
|
101
|
+
// document on parse failure; removing it defensively costs nothing.
|
|
102
|
+
document.getElementById(renderId)?.remove();
|
|
103
|
+
if (!cancelled) {
|
|
104
|
+
setState({
|
|
105
|
+
status: "failed",
|
|
106
|
+
message: error instanceof Error ? error.message : String(error),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
cancelled = true;
|
|
114
|
+
};
|
|
115
|
+
// A previously rendered diagram stays visible while a re-render (theme
|
|
116
|
+
// switch) is in flight — swapping to a blank pending state would flash.
|
|
117
|
+
}, [source, colorMode, isIncomplete]);
|
|
118
|
+
|
|
119
|
+
if (state.status === "rendered") {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
role="img"
|
|
123
|
+
aria-label="Mermaid diagram"
|
|
124
|
+
className="mb-3 last:mb-0 flex justify-center overflow-x-auto rounded-md border border-border bg-card p-3 [&_svg]:max-w-full"
|
|
125
|
+
dangerouslySetInnerHTML={{ __html: state.svg }}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<MermaidSourceFallback
|
|
132
|
+
chart={chart}
|
|
133
|
+
failureMessage={state.status === "failed" ? state.message : null}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The pre-diagram presentation of a mermaid fence: the same styled code block
|
|
140
|
+
* the shared `pre`/`code` overrides in `markdown-components.tsx` would have
|
|
141
|
+
* produced (classes mirrored from there — importing them would create a
|
|
142
|
+
* module cycle, since that file mounts this component). Shown while the fence
|
|
143
|
+
* streams, while mermaid loads, and permanently when rendering fails.
|
|
144
|
+
*/
|
|
145
|
+
function MermaidSourceFallback({
|
|
146
|
+
chart,
|
|
147
|
+
failureMessage,
|
|
148
|
+
}: {
|
|
149
|
+
chart: string;
|
|
150
|
+
failureMessage: string | null;
|
|
151
|
+
}) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="mb-3 last:mb-0">
|
|
154
|
+
<pre className="overflow-x-auto rounded-md bg-muted p-3">
|
|
155
|
+
<code className="font-mono text-xs text-foreground">{chart}</code>
|
|
156
|
+
</pre>
|
|
157
|
+
{failureMessage !== null && (
|
|
158
|
+
<p role="status" className="mt-1 text-xs text-muted-foreground">
|
|
159
|
+
Mermaid diagram failed to render; showing source. ({failureMessage})
|
|
160
|
+
</p>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Browser-mode regression guard (runs under vitest.a11y.config.ts in real
|
|
2
|
+
// Chromium) for token-driven Mermaid interior theming.
|
|
3
|
+
//
|
|
4
|
+
// This is not an axe audit — it lives here because the claim can only be
|
|
5
|
+
// verified against real layout + a real CSS engine: that Mermaid's injected
|
|
6
|
+
// `themeCSS` (authored with `var(--stgm-*)`) survives `securityLevel: "strict"`
|
|
7
|
+
// (mermaid's `sanitizeCss` + DOMPurify) and is resolved by the browser cascade
|
|
8
|
+
// against the `.stgm` scope. Source analysis already proves survivability; this
|
|
9
|
+
// test locks it against future mermaid/DOMPurify upgrades and proves, end to
|
|
10
|
+
// end, that interiors track the active color mode and preset live — with no
|
|
11
|
+
// component re-render (we mutate the scope in the DOM, never React state).
|
|
12
|
+
|
|
13
|
+
import "../../../../dist/styles.css";
|
|
14
|
+
import "@stigmer/theme/presets/fintech.css";
|
|
15
|
+
|
|
16
|
+
import { describe, it, afterEach, expect } from "vitest";
|
|
17
|
+
import { render, cleanup, waitFor } from "@testing-library/react";
|
|
18
|
+
import { ColorModeContext, type ResolvedColorMode } from "../../../color-mode.js";
|
|
19
|
+
import { MermaidDiagram } from "../../MermaidDiagram.js";
|
|
20
|
+
|
|
21
|
+
const CHART = "flowchart LR\n A[Hello] --> B[World]";
|
|
22
|
+
|
|
23
|
+
const NODE_SHAPE_SELECTOR =
|
|
24
|
+
"svg .node rect, svg .node polygon, svg .node circle, svg .node ellipse, svg .node path";
|
|
25
|
+
|
|
26
|
+
interface Mounted {
|
|
27
|
+
readonly container: HTMLElement;
|
|
28
|
+
/** The painted node shape whose `fill` our `themeCSS` drives from a token. */
|
|
29
|
+
readonly nodeShape: SVGElement;
|
|
30
|
+
/** Probe resolving `var(--stgm-muted)` (node fill) in the same scope. */
|
|
31
|
+
readonly mutedProbe: HTMLElement;
|
|
32
|
+
/** Probe resolving `var(--stgm-primary)` in the same scope. */
|
|
33
|
+
readonly primaryProbe: HTMLElement;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a diagram inside a `.stgm` scope carrying `scopeClass` (e.g. a preset)
|
|
38
|
+
* and `mode`, alongside two probe spans that resolve the same tokens. Resolves
|
|
39
|
+
* once the SVG interior has painted.
|
|
40
|
+
*/
|
|
41
|
+
async function mount(
|
|
42
|
+
scopeClass: string,
|
|
43
|
+
mode: ResolvedColorMode,
|
|
44
|
+
): Promise<Mounted> {
|
|
45
|
+
const container = document.createElement("div");
|
|
46
|
+
container.className = `stgm ${scopeClass}`.trim();
|
|
47
|
+
container.setAttribute("data-stgm-color-mode", mode);
|
|
48
|
+
document.body.appendChild(container);
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<ColorModeContext.Provider value={mode}>
|
|
52
|
+
<span data-probe="muted" style={{ color: "var(--stgm-muted)" }} />
|
|
53
|
+
<span data-probe="primary" style={{ color: "var(--stgm-primary)" }} />
|
|
54
|
+
<MermaidDiagram chart={CHART} />
|
|
55
|
+
</ColorModeContext.Provider>,
|
|
56
|
+
{ container },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
const shape = container.querySelector(NODE_SHAPE_SELECTOR);
|
|
61
|
+
if (!shape) throw new Error("diagram node not rendered yet");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
container,
|
|
66
|
+
nodeShape: container.querySelector(NODE_SHAPE_SELECTOR) as SVGElement,
|
|
67
|
+
mutedProbe: container.querySelector('[data-probe="muted"]') as HTMLElement,
|
|
68
|
+
primaryProbe: container.querySelector('[data-probe="primary"]') as HTMLElement,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
cleanup();
|
|
74
|
+
document.querySelectorAll(".stgm").forEach((node) => node.remove());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Mermaid interior theming — token-driven, cascade-resolved", () => {
|
|
78
|
+
it("paints node interiors from --stgm-* tokens, not mermaid's stock palette", async () => {
|
|
79
|
+
const { nodeShape, mutedProbe } = await mount("", "light");
|
|
80
|
+
// If our themeCSS lost the specificity/order battle, the fill would be
|
|
81
|
+
// mermaid's default (#ECECFF-ish) and this exact-match would fail.
|
|
82
|
+
expect(getComputedStyle(nodeShape).fill).toBe(
|
|
83
|
+
getComputedStyle(mutedProbe).color,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("tracks a color-mode switch live, with no re-render", async () => {
|
|
88
|
+
const { container, nodeShape, mutedProbe } = await mount("", "light");
|
|
89
|
+
const fillLight = getComputedStyle(nodeShape).fill;
|
|
90
|
+
expect(fillLight).toBe(getComputedStyle(mutedProbe).color);
|
|
91
|
+
|
|
92
|
+
// Flip the scope attribute directly — no React update. Pure cascade.
|
|
93
|
+
container.setAttribute("data-stgm-color-mode", "dark");
|
|
94
|
+
|
|
95
|
+
const fillDark = getComputedStyle(nodeShape).fill;
|
|
96
|
+
expect(fillDark).toBe(getComputedStyle(mutedProbe).color);
|
|
97
|
+
expect(fillDark).not.toBe(fillLight);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("tracks the active preset", async () => {
|
|
101
|
+
const { nodeShape, mutedProbe, primaryProbe } = await mount(
|
|
102
|
+
"stgm-theme-fintech",
|
|
103
|
+
"light",
|
|
104
|
+
);
|
|
105
|
+
// The diagram is painted from the ACTIVE preset's resolved token.
|
|
106
|
+
expect(getComputedStyle(nodeShape).fill).toBe(
|
|
107
|
+
getComputedStyle(mutedProbe).color,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Guard against a false pass where the preset CSS never loaded: fintech's
|
|
111
|
+
// primary (indigo) must differ from the default palette's primary (teal).
|
|
112
|
+
const defaultScope = document.createElement("div");
|
|
113
|
+
defaultScope.className = "stgm";
|
|
114
|
+
defaultScope.setAttribute("data-stgm-color-mode", "light");
|
|
115
|
+
defaultScope.innerHTML = '<span style="color: var(--stgm-primary)"></span>';
|
|
116
|
+
document.body.appendChild(defaultScope);
|
|
117
|
+
|
|
118
|
+
expect(getComputedStyle(primaryProbe).color).not.toBe(
|
|
119
|
+
getComputedStyle(defaultScope.firstElementChild as HTMLElement).color,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
import { render, cleanup } from "@testing-library/react";
|
|
3
3
|
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
import Markdown from "react-markdown";
|
|
4
5
|
import {
|
|
5
6
|
MARKDOWN_COMPONENTS,
|
|
7
|
+
REMARK_PLUGINS,
|
|
6
8
|
extractLeadingH1,
|
|
7
9
|
unwrapEnclosingMarkdownFence,
|
|
8
10
|
} from "../markdown-components";
|
|
9
11
|
|
|
12
|
+
// Stub the diagram component: these tests verify the DISPATCH seam (which
|
|
13
|
+
// fences route to the diagram vs the code block), not mermaid rendering —
|
|
14
|
+
// that contract is covered by mermaid-diagram.test.tsx.
|
|
15
|
+
vi.mock("../MermaidDiagram", () => ({
|
|
16
|
+
MermaidDiagram: ({ chart }: { chart: string }) => (
|
|
17
|
+
<div data-testid="mermaid-diagram">{chart}</div>
|
|
18
|
+
),
|
|
19
|
+
}));
|
|
20
|
+
|
|
10
21
|
afterEach(cleanup);
|
|
11
22
|
|
|
12
23
|
/** The shared `code` override, typed for direct rendering in tests. */
|
|
@@ -15,6 +26,11 @@ const CodeComponent = MARKDOWN_COMPONENTS.code as ComponentType<{
|
|
|
15
26
|
children?: ReactNode;
|
|
16
27
|
}>;
|
|
17
28
|
|
|
29
|
+
/** The shared `pre` override, typed for direct rendering in tests. */
|
|
30
|
+
const PreComponent = MARKDOWN_COMPONENTS.pre as ComponentType<{
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
18
34
|
describe("unwrapEnclosingMarkdownFence", () => {
|
|
19
35
|
describe("unwraps a whole-message markdown fence", () => {
|
|
20
36
|
it.each([
|
|
@@ -185,3 +201,79 @@ describe("MARKDOWN_COMPONENTS.code (shared highlight seam)", () => {
|
|
|
185
201
|
expect(code!.textContent).toBe("inlineToken");
|
|
186
202
|
});
|
|
187
203
|
});
|
|
204
|
+
|
|
205
|
+
describe("MARKDOWN_COMPONENTS.pre (mermaid dispatch seam)", () => {
|
|
206
|
+
const CHART = "flowchart LR\n A --> B";
|
|
207
|
+
|
|
208
|
+
it("routes an explicit ```mermaid fence to the diagram, dropping the <pre> wrapper", () => {
|
|
209
|
+
const { container } = render(
|
|
210
|
+
<PreComponent>
|
|
211
|
+
<code className="language-mermaid">{CHART}</code>
|
|
212
|
+
</PreComponent>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const diagram = container.querySelector('[data-testid="mermaid-diagram"]');
|
|
216
|
+
expect(diagram).not.toBeNull();
|
|
217
|
+
expect(diagram!.textContent).toBe(CHART);
|
|
218
|
+
// The diagram replaces the <pre> entirely — a block container inside
|
|
219
|
+
// <pre> would be invalid HTML and inherit code-block chrome.
|
|
220
|
+
expect(container.querySelector("pre")).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("matches language-mermaid among multiple classes", () => {
|
|
224
|
+
const { container } = render(
|
|
225
|
+
<PreComponent>
|
|
226
|
+
<code className="language-mermaid extra-class">{CHART}</code>
|
|
227
|
+
</PreComponent>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(
|
|
231
|
+
container.querySelector('[data-testid="mermaid-diagram"]'),
|
|
232
|
+
).not.toBeNull();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
[
|
|
237
|
+
"a different language",
|
|
238
|
+
<code key="go" className="language-go">{"func main() {}"}</code>,
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
"a language merely PREFIXED with mermaid",
|
|
242
|
+
<code key="pfx" className="language-mermaidjs">{CHART}</code>,
|
|
243
|
+
],
|
|
244
|
+
["an untagged fence", <code key="bare">{CHART}</code>],
|
|
245
|
+
[
|
|
246
|
+
"non-string code children",
|
|
247
|
+
<code key="node" className="language-mermaid">
|
|
248
|
+
<span>{CHART}</span>
|
|
249
|
+
</code>,
|
|
250
|
+
],
|
|
251
|
+
["plain-text children (no code element)", CHART],
|
|
252
|
+
])("keeps the ordinary <pre> code block for %s", (_label, children) => {
|
|
253
|
+
const { container } = render(<PreComponent>{children}</PreComponent>);
|
|
254
|
+
|
|
255
|
+
expect(container.querySelector('[data-testid="mermaid-diagram"]')).toBeNull();
|
|
256
|
+
const pre = container.querySelector("pre");
|
|
257
|
+
expect(pre).not.toBeNull();
|
|
258
|
+
expect(pre!.className).toContain("bg-muted");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("dispatches through real react-markdown (the artifact/skill surface path)", () => {
|
|
262
|
+
// Renders an actual document instead of synthetic elements, proving the
|
|
263
|
+
// element shape react-markdown hands to `pre` matches what the seam
|
|
264
|
+
// detects. The Streamdown (chat) path has the equivalent end-to-end
|
|
265
|
+
// coverage in message-entry.test.tsx.
|
|
266
|
+
const doc = `Before\n\n\`\`\`mermaid\n${CHART}\n\`\`\`\n\n\`\`\`go\nfunc main() {}\n\`\`\``;
|
|
267
|
+
const { container } = render(
|
|
268
|
+
<Markdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
|
|
269
|
+
{doc}
|
|
270
|
+
</Markdown>,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const diagram = container.querySelector('[data-testid="mermaid-diagram"]');
|
|
274
|
+
expect(diagram).not.toBeNull();
|
|
275
|
+
expect(diagram!.textContent).toContain("flowchart LR");
|
|
276
|
+
// The sibling go fence still renders as an ordinary highlighted block.
|
|
277
|
+
expect(container.querySelector("pre code.language-go")).not.toBeNull();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { render, cleanup, waitFor, act } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { MermaidDiagram } from "../MermaidDiagram";
|
|
5
|
+
import { loadMermaid, type MermaidModule } from "../mermaid-loader";
|
|
6
|
+
import { MERMAID_THEME_CSS } from "../mermaid-theme-css";
|
|
7
|
+
import { ColorModeContext, type ResolvedColorMode } from "../../color-mode";
|
|
8
|
+
|
|
9
|
+
// The loader is the component's only side-effectful dependency; mocking it
|
|
10
|
+
// (not the `mermaid` package) exercises the real degradation contract:
|
|
11
|
+
// a rejected load must look exactly like "library not installed".
|
|
12
|
+
vi.mock("../mermaid-loader", () => ({
|
|
13
|
+
loadMermaid: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Partial-mock streamdown so tests can drive the incomplete-fence signal
|
|
17
|
+
// without spinning up a full Streamdown render.
|
|
18
|
+
vi.mock("streamdown", async (importOriginal) => ({
|
|
19
|
+
...(await importOriginal<typeof import("streamdown")>()),
|
|
20
|
+
useIsCodeFenceIncomplete: vi.fn(() => false),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const { useIsCodeFenceIncomplete } = await import("streamdown");
|
|
24
|
+
|
|
25
|
+
const loadMermaidMock = vi.mocked(loadMermaid);
|
|
26
|
+
const incompleteFenceMock = vi.mocked(useIsCodeFenceIncomplete);
|
|
27
|
+
|
|
28
|
+
/** Builds a mermaid module stub whose render resolves with the given SVG. */
|
|
29
|
+
function stubMermaid(svg = "<svg><g>diagram</g></svg>") {
|
|
30
|
+
const mermaid = {
|
|
31
|
+
initialize: vi.fn(),
|
|
32
|
+
render: vi.fn().mockResolvedValue({ svg }),
|
|
33
|
+
};
|
|
34
|
+
loadMermaidMock.mockResolvedValue(mermaid as unknown as MermaidModule);
|
|
35
|
+
return mermaid;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderDiagram(
|
|
39
|
+
chart: string,
|
|
40
|
+
{ colorMode = "light" as ResolvedColorMode } = {},
|
|
41
|
+
) {
|
|
42
|
+
return render(
|
|
43
|
+
<ColorModeContext.Provider value={colorMode}>
|
|
44
|
+
<MermaidDiagram chart={chart} />
|
|
45
|
+
</ColorModeContext.Provider>,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function queryDiagram(container: HTMLElement) {
|
|
50
|
+
return container.querySelector('[role="img"][aria-label="Mermaid diagram"]');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
loadMermaidMock.mockReset();
|
|
55
|
+
incompleteFenceMock.mockReset();
|
|
56
|
+
incompleteFenceMock.mockReturnValue(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(cleanup);
|
|
60
|
+
|
|
61
|
+
const CHART = "flowchart LR\n A --> B";
|
|
62
|
+
|
|
63
|
+
describe("MermaidDiagram — successful render", () => {
|
|
64
|
+
it("renders the SVG in an accessible container", async () => {
|
|
65
|
+
stubMermaid("<svg data-diagram><g>ok</g></svg>");
|
|
66
|
+
const { container } = renderDiagram(CHART);
|
|
67
|
+
|
|
68
|
+
await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
|
|
69
|
+
expect(queryDiagram(container)!.querySelector("svg[data-diagram]")).not.toBeNull();
|
|
70
|
+
// The source fallback is gone once the diagram is up.
|
|
71
|
+
expect(container.querySelector("pre")).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("shows the source as a code block while the render is pending", () => {
|
|
75
|
+
// A load that never settles = the loading window.
|
|
76
|
+
loadMermaidMock.mockReturnValue(new Promise(() => {}));
|
|
77
|
+
const { container } = renderDiagram(CHART);
|
|
78
|
+
|
|
79
|
+
const code = container.querySelector("pre code");
|
|
80
|
+
expect(code).not.toBeNull();
|
|
81
|
+
expect(code!.textContent).toBe(CHART);
|
|
82
|
+
expect(queryDiagram(container)).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("trims the trailing newline the fence body carries before rendering", async () => {
|
|
86
|
+
const mermaid = stubMermaid();
|
|
87
|
+
renderDiagram("flowchart LR\n A --> B\n");
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(mermaid.render).toHaveBeenCalled());
|
|
90
|
+
expect(mermaid.render).toHaveBeenCalledWith(
|
|
91
|
+
expect.any(String),
|
|
92
|
+
"flowchart LR\n A --> B",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("MermaidDiagram — security and theming configuration", () => {
|
|
98
|
+
it("configures mermaid with securityLevel strict (agent output is untrusted)", async () => {
|
|
99
|
+
const mermaid = stubMermaid();
|
|
100
|
+
renderDiagram(CHART);
|
|
101
|
+
|
|
102
|
+
await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
|
|
103
|
+
expect(mermaid.initialize).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({ securityLevel: "strict", startOnLoad: false }),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("injects the token-driven themeCSS so interiors track --stgm-* tokens", async () => {
|
|
109
|
+
const mermaid = stubMermaid();
|
|
110
|
+
renderDiagram(CHART);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
|
|
113
|
+
// The interior palette rides on themeCSS (var(--stgm-*)), layered over the
|
|
114
|
+
// built-in default/dark base theme. The browser-mode a11y suite verifies
|
|
115
|
+
// the CSS actually resolves; here we lock the wiring itself.
|
|
116
|
+
expect(mermaid.initialize).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({ themeCSS: MERMAID_THEME_CSS }),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it.each([
|
|
122
|
+
["light", "default"],
|
|
123
|
+
["dark", "dark"],
|
|
124
|
+
] as const)("selects the %s theme for %s color mode", async (mode, theme) => {
|
|
125
|
+
const mermaid = stubMermaid();
|
|
126
|
+
renderDiagram(CHART, { colorMode: mode });
|
|
127
|
+
|
|
128
|
+
await waitFor(() => expect(mermaid.initialize).toHaveBeenCalled());
|
|
129
|
+
expect(mermaid.initialize).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({ theme }),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("re-renders the diagram when the color mode changes", async () => {
|
|
135
|
+
const mermaid = stubMermaid();
|
|
136
|
+
const { container, rerender } = render(
|
|
137
|
+
<ColorModeContext.Provider value="light">
|
|
138
|
+
<MermaidDiagram chart={CHART} />
|
|
139
|
+
</ColorModeContext.Provider>,
|
|
140
|
+
);
|
|
141
|
+
await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
|
|
142
|
+
|
|
143
|
+
rerender(
|
|
144
|
+
<ColorModeContext.Provider value="dark">
|
|
145
|
+
<MermaidDiagram chart={CHART} />
|
|
146
|
+
</ColorModeContext.Provider>,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await waitFor(() => expect(mermaid.render).toHaveBeenCalledTimes(2));
|
|
150
|
+
expect(mermaid.initialize).toHaveBeenLastCalledWith(
|
|
151
|
+
expect.objectContaining({ theme: "dark" }),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("MermaidDiagram — failure degrades to the code block", () => {
|
|
157
|
+
it("falls back with an announced note when the diagram source is invalid", async () => {
|
|
158
|
+
loadMermaidMock.mockResolvedValue({
|
|
159
|
+
initialize: vi.fn(),
|
|
160
|
+
render: vi.fn().mockRejectedValue(new Error("Parse error on line 2")),
|
|
161
|
+
} as unknown as MermaidModule);
|
|
162
|
+
const { container } = renderDiagram("flowchart LR\n A --> ");
|
|
163
|
+
|
|
164
|
+
await waitFor(() =>
|
|
165
|
+
expect(container.querySelector('[role="status"]')).not.toBeNull(),
|
|
166
|
+
);
|
|
167
|
+
expect(container.querySelector("pre code")!.textContent).toContain(
|
|
168
|
+
"A -->",
|
|
169
|
+
);
|
|
170
|
+
expect(container.querySelector('[role="status"]')!.textContent).toContain(
|
|
171
|
+
"Parse error on line 2",
|
|
172
|
+
);
|
|
173
|
+
expect(queryDiagram(container)).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("falls back when the mermaid library cannot be loaded (optional peer absent)", async () => {
|
|
177
|
+
loadMermaidMock.mockRejectedValue(
|
|
178
|
+
new Error("Cannot find module 'mermaid'"),
|
|
179
|
+
);
|
|
180
|
+
const { container } = renderDiagram(CHART);
|
|
181
|
+
|
|
182
|
+
await waitFor(() =>
|
|
183
|
+
expect(container.querySelector('[role="status"]')).not.toBeNull(),
|
|
184
|
+
);
|
|
185
|
+
expect(container.querySelector("pre code")!.textContent).toBe(CHART);
|
|
186
|
+
expect(queryDiagram(container)).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("does not attempt to render an empty fence", () => {
|
|
190
|
+
stubMermaid();
|
|
191
|
+
const { container } = renderDiagram(" \n ");
|
|
192
|
+
|
|
193
|
+
expect(loadMermaidMock).not.toHaveBeenCalled();
|
|
194
|
+
expect(queryDiagram(container)).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("MermaidDiagram — streaming deferral", () => {
|
|
199
|
+
it("defers rendering while the fence is still streaming", () => {
|
|
200
|
+
stubMermaid();
|
|
201
|
+
incompleteFenceMock.mockReturnValue(true);
|
|
202
|
+
const { container } = renderDiagram(CHART);
|
|
203
|
+
|
|
204
|
+
expect(loadMermaidMock).not.toHaveBeenCalled();
|
|
205
|
+
const code = container.querySelector("pre code");
|
|
206
|
+
expect(code).not.toBeNull();
|
|
207
|
+
expect(code!.textContent).toBe(CHART);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("renders the diagram once the fence closes", async () => {
|
|
211
|
+
stubMermaid();
|
|
212
|
+
incompleteFenceMock.mockReturnValue(true);
|
|
213
|
+
const { container, rerender } = renderDiagram(CHART);
|
|
214
|
+
expect(queryDiagram(container)).toBeNull();
|
|
215
|
+
|
|
216
|
+
incompleteFenceMock.mockReturnValue(false);
|
|
217
|
+
rerender(
|
|
218
|
+
<ColorModeContext.Provider value="light">
|
|
219
|
+
<MermaidDiagram chart={CHART} />
|
|
220
|
+
</ColorModeContext.Provider>,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await waitFor(() => expect(queryDiagram(container)).not.toBeNull());
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("MermaidDiagram — stale-render races", () => {
|
|
228
|
+
it("discards a render that resolves after the chart has changed", async () => {
|
|
229
|
+
const initialize = vi.fn();
|
|
230
|
+
let resolveFirst!: (result: { svg: string }) => void;
|
|
231
|
+
const renderMock = vi
|
|
232
|
+
.fn()
|
|
233
|
+
.mockImplementationOnce(
|
|
234
|
+
() => new Promise((resolve) => (resolveFirst = resolve)),
|
|
235
|
+
)
|
|
236
|
+
.mockResolvedValue({ svg: "<svg data-chart='second'></svg>" });
|
|
237
|
+
loadMermaidMock.mockResolvedValue({
|
|
238
|
+
initialize,
|
|
239
|
+
render: renderMock,
|
|
240
|
+
} as unknown as MermaidModule);
|
|
241
|
+
|
|
242
|
+
const { container, rerender } = renderDiagram("flowchart LR\n A --> B");
|
|
243
|
+
await waitFor(() => expect(renderMock).toHaveBeenCalledTimes(1));
|
|
244
|
+
|
|
245
|
+
rerender(
|
|
246
|
+
<ColorModeContext.Provider value="light">
|
|
247
|
+
<MermaidDiagram chart={"flowchart LR\n C --> D"} />
|
|
248
|
+
</ColorModeContext.Provider>,
|
|
249
|
+
);
|
|
250
|
+
await waitFor(() =>
|
|
251
|
+
expect(
|
|
252
|
+
container.querySelector("svg[data-chart='second']"),
|
|
253
|
+
).not.toBeNull(),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// The first (now stale) render settles late — it must not clobber the
|
|
257
|
+
// second chart's SVG.
|
|
258
|
+
act(() => resolveFirst({ svg: "<svg data-chart='first'></svg>" }));
|
|
259
|
+
expect(container.querySelector("svg[data-chart='first']")).toBeNull();
|
|
260
|
+
expect(container.querySelector("svg[data-chart='second']")).not.toBeNull();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("ignores a render that settles after unmount", async () => {
|
|
264
|
+
let resolveRender!: (result: { svg: string }) => void;
|
|
265
|
+
loadMermaidMock.mockResolvedValue({
|
|
266
|
+
initialize: vi.fn(),
|
|
267
|
+
render: vi.fn().mockReturnValue(
|
|
268
|
+
new Promise((resolve) => (resolveRender = resolve)),
|
|
269
|
+
),
|
|
270
|
+
} as unknown as MermaidModule);
|
|
271
|
+
|
|
272
|
+
const { unmount } = renderDiagram(CHART);
|
|
273
|
+
await waitFor(() => expect(loadMermaidMock).toHaveBeenCalled());
|
|
274
|
+
unmount();
|
|
275
|
+
|
|
276
|
+
// Settling after unmount must not warn or throw (React state update on
|
|
277
|
+
// an unmounted component).
|
|
278
|
+
expect(() =>
|
|
279
|
+
act(() => resolveRender({ svg: "<svg></svg>" })),
|
|
280
|
+
).not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
});
|