@stigmer/react 3.1.3 → 3.1.5
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/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +13 -7
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +7 -4
- 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/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- 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/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- 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 +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +101 -21
- 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 +3 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +5 -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/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- 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/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- 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__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +27 -5
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +15 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- 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 +204 -35
- 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/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- 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 +7 -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/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
3
5
|
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
6
|
+
import { toDisplayFileChange } from "@stigmer/sdk";
|
|
4
7
|
import type { ToolResultView } from "@stigmer/sdk";
|
|
5
8
|
import { cn } from "@stigmer/theme";
|
|
6
9
|
import { BoundedContent } from "../internal/BoundedContent.js";
|
|
10
|
+
import { FileChangeDiff } from "./FileChangesView.js";
|
|
11
|
+
import { useFileReviewRowChange } from "./FileReviewContext.js";
|
|
7
12
|
import { McpToolDetail } from "./McpToolDetail.js";
|
|
8
13
|
import { ToolArgsView } from "./ToolArgsView.js";
|
|
9
14
|
import { ResultView } from "./ResultView.js";
|
|
@@ -43,8 +48,9 @@ export interface ToolCallDetailProps {
|
|
|
43
48
|
* degrade to readable JSON instead of a raw dump.
|
|
44
49
|
*
|
|
45
50
|
* Composition varies by category to avoid redundancy: an edit shows only the
|
|
46
|
-
* diff (which already names the file and quantifies the change); a
|
|
47
|
-
* its
|
|
51
|
+
* diff (which already names the file and quantifies the change); a stamped
|
|
52
|
+
* write shows its change set's captured diff, degrading to the proposed
|
|
53
|
+
* content when no capture resolves; a read shows just the path.
|
|
48
54
|
*
|
|
49
55
|
* Importable on its own by platform builders composing custom tool UIs.
|
|
50
56
|
*
|
|
@@ -60,6 +66,16 @@ export function ToolCallDetail({
|
|
|
60
66
|
}: ToolCallDetailProps) {
|
|
61
67
|
const { category, result, primaryArg } = useToolPresentation(toolCall);
|
|
62
68
|
|
|
69
|
+
// The captured net change a stamped write row renders as its inline diff
|
|
70
|
+
// (see the write branch below). Resolved here unconditionally (Rules of
|
|
71
|
+
// Hooks); null for every other category's rows — they are never stamped —
|
|
72
|
+
// and for every honest-degradation case, so this costs nothing outside the
|
|
73
|
+
// stamped-write path.
|
|
74
|
+
const capturedChange = useFileReviewRowChange(
|
|
75
|
+
toolCall.fileChangeSetId,
|
|
76
|
+
primaryArg,
|
|
77
|
+
);
|
|
78
|
+
|
|
63
79
|
return (
|
|
64
80
|
<div className={cn("space-y-2 text-xs", className)}>
|
|
65
81
|
<CategoryDetail
|
|
@@ -68,6 +84,7 @@ export function ToolCallDetail({
|
|
|
68
84
|
result={result}
|
|
69
85
|
primaryArg={primaryArg}
|
|
70
86
|
primaryArgTruncated={primaryArgTruncated}
|
|
87
|
+
capturedChange={capturedChange}
|
|
71
88
|
/>
|
|
72
89
|
</div>
|
|
73
90
|
);
|
|
@@ -90,12 +107,14 @@ function CategoryDetail({
|
|
|
90
107
|
result,
|
|
91
108
|
primaryArg,
|
|
92
109
|
primaryArgTruncated,
|
|
110
|
+
capturedChange,
|
|
93
111
|
}: {
|
|
94
112
|
toolCall: ToolCall;
|
|
95
113
|
category: ToolCategory;
|
|
96
114
|
result: ToolResultView;
|
|
97
115
|
primaryArg: string | null;
|
|
98
116
|
primaryArgTruncated: boolean;
|
|
117
|
+
capturedChange: CapturedFileChange | null;
|
|
99
118
|
}) {
|
|
100
119
|
const args = <ArgsSection toolCall={toolCall} />;
|
|
101
120
|
|
|
@@ -128,27 +147,50 @@ function CategoryDetail({
|
|
|
128
147
|
);
|
|
129
148
|
|
|
130
149
|
case "edit":
|
|
131
|
-
case "write":
|
|
150
|
+
case "write": {
|
|
132
151
|
// The diff already names the file and quantifies the change, so it stands
|
|
133
|
-
// alone — showing the written content as an argument would duplicate it.
|
|
134
|
-
//
|
|
135
|
-
// (ResultView shows the content), so the input is never lost. The owning
|
|
136
|
-
// row already names the file, so the body suppresses the path.
|
|
152
|
+
// alone — showing the written content as an argument would duplicate it.
|
|
153
|
+
// The owning row already names the file, so the body suppresses the path.
|
|
137
154
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
155
|
+
// An EDIT's diff is reconstructed per-call from its own args (old/new
|
|
156
|
+
// strings, or the Cursor envelope's unified diff). A whole-file WRITE has
|
|
157
|
+
// no per-call before-image in its args, so a stamped write renders the
|
|
158
|
+
// captured NET change from its change set instead — the same artifact the
|
|
159
|
+
// row's review badge refers to, resolved through the same path matcher so
|
|
160
|
+
// the two can never disagree. An unresolvable write (still streaming
|
|
161
|
+
// before the turn-boundary capture, no capture mode, legacy session, or a
|
|
162
|
+
// non-reviewable change) keeps the honest fallback: the proposed content
|
|
163
|
+
// from the args, as a `file` view.
|
|
164
|
+
if (category === "write" && capturedChange) {
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<ProvenanceNote toolCall={toolCall} />
|
|
168
|
+
<CapturedRowDiff change={capturedChange} />
|
|
169
|
+
</>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
// Bounding is keyed on the VIEW, not the category: BoundedContent is for
|
|
173
|
+
// bodies with no internal truncation of their own — the diff and a FAILED
|
|
174
|
+
// call's error `<pre>` — giving a large or still-streaming edit the same
|
|
175
|
+
// scannable clamp the approval gate uses. The `file` view self-truncates
|
|
176
|
+
// (CollapsibleCode's line cap), so wrapping it would stack two reveal
|
|
177
|
+
// controls on one block — exactly what BoundedContent's contract forbids.
|
|
178
|
+
const body = (
|
|
179
|
+
<ResultView view={result} showFileName={false} showStats={false} />
|
|
180
|
+
);
|
|
144
181
|
return (
|
|
145
182
|
<>
|
|
146
183
|
<ProvenanceNote toolCall={toolCall} />
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
{result.type === "file" ? (
|
|
185
|
+
body
|
|
186
|
+
) : (
|
|
187
|
+
<BoundedContent cursorTarget="tool-detail-expand">
|
|
188
|
+
{body}
|
|
189
|
+
</BoundedContent>
|
|
190
|
+
)}
|
|
150
191
|
</>
|
|
151
192
|
);
|
|
193
|
+
}
|
|
152
194
|
|
|
153
195
|
case "read":
|
|
154
196
|
case "delete":
|
|
@@ -245,6 +287,17 @@ function ThinkToolDetail({ toolCall }: { toolCall: ToolCall }) {
|
|
|
245
287
|
// Shared sub-components
|
|
246
288
|
// ---------------------------------------------------------------------------
|
|
247
289
|
|
|
290
|
+
// A stamped write row's inline diff: the captured change projected onto the
|
|
291
|
+
// display FileChange and rendered through the same bounded FileChangeDiff the
|
|
292
|
+
// review card's body uses — offloaded-body fetch, binary notices, and
|
|
293
|
+
// truncation states included. The file name is suppressed (the owning row
|
|
294
|
+
// header names it); the `+N −M` stats stay because a write row's header
|
|
295
|
+
// summary carries none.
|
|
296
|
+
function CapturedRowDiff({ change }: { change: CapturedFileChange }) {
|
|
297
|
+
const adapted = useMemo(() => toDisplayFileChange(change), [change]);
|
|
298
|
+
return <FileChangeDiff change={adapted} bounded showFileName={false} />;
|
|
299
|
+
}
|
|
300
|
+
|
|
248
301
|
// The detail body's only metadata. Duration and the MCP slug live in the owning
|
|
249
302
|
// row header (ToolCallItem / ApprovalCardHeader), so the body never restates
|
|
250
303
|
// them; it surfaces the authorization provenance the runner stamped onto the
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
5
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
6
|
+
import { ArtifactContentBody } from "../ArtifactContentBody";
|
|
7
|
+
import type { SkillPackageDetection } from "../../library/detect-skill-package";
|
|
8
|
+
|
|
9
|
+
const NOT_SKILL: SkillPackageDetection = { detected: false };
|
|
10
|
+
|
|
11
|
+
function fileArtifact(name: string) {
|
|
12
|
+
return create(ExecutionArtifactSchema, {
|
|
13
|
+
name,
|
|
14
|
+
kind: ExecutionArtifactKind.FILE,
|
|
15
|
+
sizeBytes: 64n,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dirArtifact(name: string, entries: string[]) {
|
|
20
|
+
return create(ExecutionArtifactSchema, {
|
|
21
|
+
name,
|
|
22
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
23
|
+
sizeBytes: 512n,
|
|
24
|
+
entries,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(cleanup);
|
|
29
|
+
|
|
30
|
+
describe("ArtifactContentBody — file states", () => {
|
|
31
|
+
it("renders a loading skeleton", () => {
|
|
32
|
+
render(
|
|
33
|
+
<ArtifactContentBody
|
|
34
|
+
artifact={fileArtifact("notes.txt")}
|
|
35
|
+
content={null}
|
|
36
|
+
contentType={null}
|
|
37
|
+
isLoading
|
|
38
|
+
error={null}
|
|
39
|
+
isTruncated={false}
|
|
40
|
+
skillDetection={NOT_SKILL}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
expect(screen.getByLabelText("Loading content")).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders an error message", () => {
|
|
47
|
+
render(
|
|
48
|
+
<ArtifactContentBody
|
|
49
|
+
artifact={fileArtifact("notes.txt")}
|
|
50
|
+
content={null}
|
|
51
|
+
contentType={null}
|
|
52
|
+
isLoading={false}
|
|
53
|
+
error={new Error("boom")}
|
|
54
|
+
isTruncated={false}
|
|
55
|
+
skillDetection={NOT_SKILL}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
expect(screen.getByText("boom")).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders the binary / unavailable state when content is null and settled", () => {
|
|
62
|
+
render(
|
|
63
|
+
<ArtifactContentBody
|
|
64
|
+
artifact={fileArtifact("image.bin")}
|
|
65
|
+
content={null}
|
|
66
|
+
contentType={null}
|
|
67
|
+
isLoading={false}
|
|
68
|
+
error={null}
|
|
69
|
+
isTruncated={false}
|
|
70
|
+
skillDetection={NOT_SKILL}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByText(/Content not available for preview/)).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("renders text content", () => {
|
|
77
|
+
render(
|
|
78
|
+
<ArtifactContentBody
|
|
79
|
+
artifact={fileArtifact("notes.txt")}
|
|
80
|
+
content={"hello from the artifact"}
|
|
81
|
+
contentType={"text/plain"}
|
|
82
|
+
isLoading={false}
|
|
83
|
+
error={null}
|
|
84
|
+
isTruncated={false}
|
|
85
|
+
skillDetection={NOT_SKILL}
|
|
86
|
+
/>,
|
|
87
|
+
);
|
|
88
|
+
expect(screen.getByText(/hello from the artifact/)).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("ArtifactContentBody — directory state", () => {
|
|
93
|
+
it("lists directory entries", () => {
|
|
94
|
+
render(
|
|
95
|
+
<ArtifactContentBody
|
|
96
|
+
artifact={dirArtifact("pack", ["SKILL.md", "run.sh"])}
|
|
97
|
+
content={null}
|
|
98
|
+
contentType={null}
|
|
99
|
+
isLoading={false}
|
|
100
|
+
error={null}
|
|
101
|
+
isTruncated={false}
|
|
102
|
+
skillDetection={NOT_SKILL}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
expect(screen.getByText("Files (2)")).toBeTruthy();
|
|
106
|
+
expect(screen.getByText("SKILL.md")).toBeTruthy();
|
|
107
|
+
expect(screen.getByText("run.sh")).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("shows the skill banner when a skill package is detected", () => {
|
|
111
|
+
const detection = {
|
|
112
|
+
detected: true,
|
|
113
|
+
skillName: "My Skill",
|
|
114
|
+
skillDescription: "does things",
|
|
115
|
+
fileCount: 2,
|
|
116
|
+
} as unknown as SkillPackageDetection;
|
|
117
|
+
render(
|
|
118
|
+
<ArtifactContentBody
|
|
119
|
+
artifact={dirArtifact("pack", ["SKILL.md"])}
|
|
120
|
+
content={null}
|
|
121
|
+
contentType={null}
|
|
122
|
+
isLoading={false}
|
|
123
|
+
error={null}
|
|
124
|
+
isTruncated={false}
|
|
125
|
+
skillDetection={detection}
|
|
126
|
+
/>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByText("My Skill")).toBeTruthy();
|
|
129
|
+
expect(screen.getByText("does things")).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { ArtifactDocument } from "../ArtifactDocument";
|
|
9
|
+
|
|
10
|
+
function fileArtifact(name: string) {
|
|
11
|
+
return create(ExecutionArtifactSchema, {
|
|
12
|
+
name,
|
|
13
|
+
kind: ExecutionArtifactKind.FILE,
|
|
14
|
+
sizeBytes: 64n,
|
|
15
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
16
|
+
contentHash: "hash-1",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirArtifact(name: string) {
|
|
21
|
+
return create(ExecutionArtifactSchema, {
|
|
22
|
+
name,
|
|
23
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
24
|
+
sizeBytes: 512n,
|
|
25
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
26
|
+
entries: ["a.txt", "b.txt"],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stigmerReturning(text: string): Stigmer {
|
|
31
|
+
return {
|
|
32
|
+
agentExecution: {
|
|
33
|
+
getArtifactContent: vi.fn().mockResolvedValue({
|
|
34
|
+
content: new TextEncoder().encode(text),
|
|
35
|
+
contentType: "text/plain",
|
|
36
|
+
truncated: false,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
} as unknown as Stigmer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderDoc(stigmer: Stigmer, artifact = fileArtifact("notes.txt")) {
|
|
43
|
+
return render(
|
|
44
|
+
<StigmerContext.Provider value={stigmer}>
|
|
45
|
+
<ArtifactDocument
|
|
46
|
+
artifact={artifact}
|
|
47
|
+
executionId="aex_1"
|
|
48
|
+
org="acme"
|
|
49
|
+
isTerminal
|
|
50
|
+
/>
|
|
51
|
+
</StigmerContext.Provider>,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const AGENT_YAML = [
|
|
56
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
57
|
+
"kind: Agent",
|
|
58
|
+
"metadata:",
|
|
59
|
+
" name: my-agent",
|
|
60
|
+
"spec: {}",
|
|
61
|
+
"",
|
|
62
|
+
].join("\n");
|
|
63
|
+
|
|
64
|
+
afterEach(cleanup);
|
|
65
|
+
|
|
66
|
+
describe("ArtifactDocument", () => {
|
|
67
|
+
it("renders the toolbar (name + Download) and the fetched content", async () => {
|
|
68
|
+
renderDoc(stigmerReturning("hello document"));
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole("article", { name: "Artifact notes.txt" })).toBeTruthy();
|
|
71
|
+
expect(screen.getByText("notes.txt")).toBeTruthy();
|
|
72
|
+
expect(screen.getByText("Download")).toBeTruthy();
|
|
73
|
+
await waitFor(() => expect(screen.getByText(/hello document/)).toBeTruthy());
|
|
74
|
+
expect(screen.getByText("Copy")).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("surfaces resource detection and an Apply CTA for a detected Agent", async () => {
|
|
78
|
+
renderDoc(stigmerReturning(AGENT_YAML), fileArtifact("agent.yaml"));
|
|
79
|
+
|
|
80
|
+
await waitFor(() => expect(screen.getByText("Agent detected")).toBeTruthy());
|
|
81
|
+
expect(screen.getByText("Apply to acme")).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("renders a directory artifact with a Download ZIP action and no Copy", () => {
|
|
85
|
+
render(
|
|
86
|
+
<StigmerContext.Provider value={stigmerReturning("")}>
|
|
87
|
+
<ArtifactDocument
|
|
88
|
+
artifact={dirArtifact("pack")}
|
|
89
|
+
executionId="aex_1"
|
|
90
|
+
org="acme"
|
|
91
|
+
isTerminal
|
|
92
|
+
/>
|
|
93
|
+
</StigmerContext.Provider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText("Download ZIP")).toBeTruthy();
|
|
97
|
+
expect(screen.queryByText("Copy")).toBeNull();
|
|
98
|
+
expect(screen.getByText("Files (2)")).toBeTruthy();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
|
+
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
7
|
+
import { StigmerContext } from "../../context";
|
|
8
|
+
import { ArtifactRow } from "../ArtifactRow";
|
|
9
|
+
|
|
10
|
+
function fileArtifact(name: string, sandboxPath = `.stigmer/${name}`) {
|
|
11
|
+
return create(ExecutionArtifactSchema, {
|
|
12
|
+
name,
|
|
13
|
+
kind: ExecutionArtifactKind.FILE,
|
|
14
|
+
sizeBytes: 2048n,
|
|
15
|
+
sandboxPath,
|
|
16
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dirArtifact(name: string) {
|
|
21
|
+
return create(ExecutionArtifactSchema, {
|
|
22
|
+
name,
|
|
23
|
+
kind: ExecutionArtifactKind.DIRECTORY,
|
|
24
|
+
sizeBytes: 4096n,
|
|
25
|
+
sandboxPath: `.stigmer/${name}`,
|
|
26
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getArtifactDownloadUrl = vi.fn().mockResolvedValue({ downloadUrl: "" });
|
|
31
|
+
|
|
32
|
+
function createStigmerMock(): Stigmer {
|
|
33
|
+
return {
|
|
34
|
+
agentExecution: { getArtifactDownloadUrl },
|
|
35
|
+
} as unknown as Stigmer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderRow(props: Partial<Parameters<typeof ArtifactRow>[0]> = {}) {
|
|
39
|
+
const merged = {
|
|
40
|
+
artifact: fileArtifact("notes.md"),
|
|
41
|
+
executionId: "aex_1",
|
|
42
|
+
onOpen: vi.fn(),
|
|
43
|
+
...props,
|
|
44
|
+
} as Parameters<typeof ArtifactRow>[0];
|
|
45
|
+
return render(
|
|
46
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
47
|
+
<ul>
|
|
48
|
+
<ArtifactRow {...merged} />
|
|
49
|
+
</ul>
|
|
50
|
+
</StigmerContext.Provider>,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
cleanup();
|
|
56
|
+
getArtifactDownloadUrl.mockClear();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("ArtifactRow — presentation", () => {
|
|
60
|
+
it("renders a file-type icon, the name, and the formatted size", () => {
|
|
61
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
62
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
63
|
+
expect(open.querySelector("svg")).toBeTruthy();
|
|
64
|
+
expect(screen.getByText("2.0 KB")).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("appends a trailing slash for a directory artifact", () => {
|
|
68
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
69
|
+
// The name span's own text is "return-policy" + "/".
|
|
70
|
+
expect(screen.getByText("return-policy/")).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the parent-directory subtitle only on a name collision", () => {
|
|
74
|
+
const artifact = fileArtifact("agent.yaml", "/workspace/configs/agent.yaml");
|
|
75
|
+
const { rerender } = renderRow({ artifact, hasNameCollision: true });
|
|
76
|
+
expect(screen.getByText("configs/")).toBeTruthy();
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
80
|
+
<ul>
|
|
81
|
+
<ArtifactRow artifact={artifact} executionId="aex_1" onOpen={vi.fn()} />
|
|
82
|
+
</ul>
|
|
83
|
+
</StigmerContext.Provider>,
|
|
84
|
+
);
|
|
85
|
+
expect(screen.queryByText("configs/")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("ArtifactRow — nested-interactive avoidance (DD-22)", () => {
|
|
90
|
+
it("renders the Download control as a SIBLING of the open button, never nested", () => {
|
|
91
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
92
|
+
const open = screen.getByText("notes.md").closest("button")!;
|
|
93
|
+
const download = screen.getByLabelText("Download notes.md");
|
|
94
|
+
expect(download.tagName).toBe("BUTTON");
|
|
95
|
+
// A <button> inside a <button> is a WCAG 4.1.2 nested-interactive violation.
|
|
96
|
+
expect(open.contains(download)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("ArtifactRow — open / activate interaction (file-tree parity)", () => {
|
|
101
|
+
it("fires onOpen on a single click of the open target", () => {
|
|
102
|
+
const onOpen = vi.fn();
|
|
103
|
+
renderRow({ onOpen });
|
|
104
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
105
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("fires onActivate on a double-click when provided", () => {
|
|
109
|
+
const onActivate = vi.fn();
|
|
110
|
+
renderRow({ onActivate });
|
|
111
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
112
|
+
expect(onActivate).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("binds no double-click handler when onActivate is omitted (harmless)", () => {
|
|
116
|
+
const onOpen = vi.fn();
|
|
117
|
+
renderRow({ onOpen });
|
|
118
|
+
// With no onActivate, a dblclick fires nothing (onOpen only responds to click).
|
|
119
|
+
fireEvent.doubleClick(screen.getByText("notes.md"));
|
|
120
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("ArtifactRow — download", () => {
|
|
125
|
+
it("mints a download URL for the artifact's storage key on Download click", () => {
|
|
126
|
+
renderRow({ artifact: fileArtifact("notes.md") });
|
|
127
|
+
fireEvent.click(screen.getByLabelText("Download notes.md"));
|
|
128
|
+
expect(getArtifactDownloadUrl).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(getArtifactDownloadUrl.mock.calls[0][0]).toMatchObject({
|
|
130
|
+
executionId: "aex_1",
|
|
131
|
+
storageKey: "artifacts/aex_1/notes.md",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("labels the Download control 'Download ZIP' for a directory", () => {
|
|
136
|
+
renderRow({ artifact: dirArtifact("return-policy") });
|
|
137
|
+
expect(screen.getByTitle("Download ZIP")).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
7
|
+
import {
|
|
8
|
+
ExecutionArtifactKind,
|
|
9
|
+
ExecutionPhase,
|
|
10
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
|
+
import { StigmerContext } from "../../context";
|
|
12
|
+
import { ArtifactsWidget } from "../ArtifactsWidget";
|
|
13
|
+
|
|
14
|
+
function artifact(name: string) {
|
|
15
|
+
return create(ExecutionArtifactSchema, {
|
|
16
|
+
name,
|
|
17
|
+
kind: ExecutionArtifactKind.FILE,
|
|
18
|
+
sizeBytes: 1024n,
|
|
19
|
+
sandboxPath: `.stigmer/${name}`,
|
|
20
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const execution = create(AgentExecutionSchema, {
|
|
25
|
+
metadata: { id: "aex_1" },
|
|
26
|
+
status: {
|
|
27
|
+
phase: ExecutionPhase.EXECUTION_COMPLETED,
|
|
28
|
+
artifacts: [artifact("agent.yaml"), artifact("notes.md")],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Modal content fetch — keep pending so nothing rejects during the test. */
|
|
33
|
+
function createStigmerMock(): Stigmer {
|
|
34
|
+
return {
|
|
35
|
+
agentExecution: {
|
|
36
|
+
getArtifactContent: vi.fn().mockReturnValue(new Promise(() => {})),
|
|
37
|
+
getArtifactDownloadUrl: vi.fn().mockResolvedValue({ downloadUrl: "" }),
|
|
38
|
+
},
|
|
39
|
+
} as unknown as Stigmer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderWidget(executions = [execution]) {
|
|
43
|
+
return render(
|
|
44
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
45
|
+
<ArtifactsWidget executions={executions} org="acme" />
|
|
46
|
+
</StigmerContext.Provider>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(cleanup);
|
|
51
|
+
|
|
52
|
+
describe("ArtifactsWidget — dense list (panel-less)", () => {
|
|
53
|
+
it("renders nothing when there are no artifacts", () => {
|
|
54
|
+
const { container } = renderWidget([]);
|
|
55
|
+
expect(container.firstChild).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders a heading, count, and one dense row per artifact (no ArtifactCard chrome)", () => {
|
|
59
|
+
renderWidget();
|
|
60
|
+
expect(screen.getByRole("heading", { name: "Artifacts" })).toBeTruthy();
|
|
61
|
+
// Count badge reflects the two deduped artifacts.
|
|
62
|
+
expect(screen.getByText("2")).toBeTruthy();
|
|
63
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
64
|
+
expect(screen.getByText("agent.yaml")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
66
|
+
// The retired ArtifactCard rendered a "Preview" button and role="article".
|
|
67
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
68
|
+
expect(screen.queryByRole("article")).toBeNull();
|
|
69
|
+
expect(screen.getByLabelText("Download agent.yaml")).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("opens the preview modal (the panel-less fallback) on a row click", () => {
|
|
73
|
+
renderWidget();
|
|
74
|
+
expect(document.querySelector("dialog")).toBeNull();
|
|
75
|
+
fireEvent.click(screen.getByText("notes.md"));
|
|
76
|
+
expect(document.querySelector("dialog")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
});
|