@runtypelabs/persona 3.10.0 → 3.11.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.
@@ -210,9 +210,12 @@ export const THEME_TOKEN_DOCS = {
210
210
  'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
211
211
  },
212
212
  toolCall: {
213
- description: 'Tool call display styling and collapsed/grouped rendering hooks.',
213
+ description:
214
+ 'Tool call display styling, text templates, loading animations, and rendering hooks. ' +
215
+ 'Text templates support placeholders ({toolName}, {duration}) and inline formatting (~dim~, *italic*, **bold**). ' +
216
+ 'renderCollapsedSummary receives elapsed (static string) and createElapsedElement() (live-updating span) in its context.',
214
217
  properties:
215
- 'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor, renderCollapsedSummary, renderCollapsedPreview, renderGroupedSummary.',
218
+ 'shadow, backgroundColor, borderColor, borderWidth, borderRadius, headerBackgroundColor, headerTextColor, headerPaddingX, headerPaddingY, contentBackgroundColor, contentTextColor, contentPaddingX, contentPaddingY, codeBlockBackgroundColor, codeBlockBorderColor, codeBlockTextColor, toggleTextColor, labelTextColor, activeTextTemplate, completeTextTemplate, loadingAnimationColor, loadingAnimationSecondaryColor, loadingAnimationDuration, renderCollapsedSummary, renderCollapsedPreview, renderGroupedSummary.',
216
219
  },
217
220
  reasoning: {
218
221
  description: 'Reasoning/thinking row rendering hooks.',
@@ -282,7 +285,7 @@ export const THEME_TOKEN_DOCS = {
282
285
  features: {
283
286
  description: 'Feature flags.',
284
287
  properties:
285
- 'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines), artifacts (sidebar config).',
288
+ 'showReasoning (AI thinking steps), showToolCalls (tool invocations), toolCallDisplay (collapsedMode, activePreview, activeMinHeight, previewMaxLines, grouped, expandable, loadingAnimation), reasoningDisplay (activePreview, activeMinHeight, previewMaxLines, expandable), artifacts (sidebar config).',
286
289
  },
287
290
  },
288
291
  }
@@ -10,6 +10,7 @@ describe("tool call display defaults", () => {
10
10
  grouped: false,
11
11
  previewMaxLines: 3,
12
12
  expandable: true,
13
+ loadingAnimation: "none",
13
14
  });
14
15
  });
15
16
 
package/src/types.ts CHANGED
@@ -577,6 +577,19 @@ export type AgentWidgetToolCallCollapsedMode =
577
577
  | "tool-name"
578
578
  | "tool-preview";
579
579
 
580
+ /**
581
+ * Animation mode applied to tool call header text while the tool is running.
582
+ * Character-by-character modes (`shimmer`, `shimmer-color`, `rainbow`) wrap each
583
+ * character in a span with staggered `animation-delay`. `pulse` applies to the
584
+ * entire text container. Honors `prefers-reduced-motion`.
585
+ */
586
+ export type AgentWidgetToolCallLoadingAnimation =
587
+ | "none"
588
+ | "pulse"
589
+ | "shimmer"
590
+ | "shimmer-color"
591
+ | "rainbow";
592
+
580
593
  export type AgentWidgetToolCallDisplayFeature = {
581
594
  /**
582
595
  * Controls what collapsed tool call rows show in their header/summary area.
@@ -590,6 +603,8 @@ export type AgentWidgetToolCallDisplayFeature = {
590
603
  activePreview?: boolean;
591
604
  /**
592
605
  * Optional CSS min-height applied to active collapsed tool call rows.
606
+ * @default undefined (no min-height)
607
+ * @example "100px"
593
608
  */
594
609
  activeMinHeight?: string;
595
610
  /**
@@ -608,6 +623,16 @@ export type AgentWidgetToolCallDisplayFeature = {
608
623
  * @default true
609
624
  */
610
625
  expandable?: boolean;
626
+ /**
627
+ * Animation mode applied to the tool call header text while the tool is active.
628
+ * - "none" — static text, no animation
629
+ * - "pulse" — opacity pulse on the entire header text
630
+ * - "shimmer" — monochrome opacity sweep per character
631
+ * - "shimmer-color" — color gradient sweep per character
632
+ * - "rainbow" — rainbow color cycle per character
633
+ * @default "none"
634
+ */
635
+ loadingAnimation?: AgentWidgetToolCallLoadingAnimation;
611
636
  };
612
637
 
613
638
  export type AgentWidgetReasoningDisplayFeature = {
@@ -1234,22 +1259,39 @@ export type AgentWidgetApprovalConfig = {
1234
1259
  export type AgentWidgetToolCallConfig = {
1235
1260
  /** Box-shadow for tool-call bubbles; overrides `theme.toolBubbleShadow` when set. */
1236
1261
  shadow?: string;
1262
+ /** Background color of the tool call bubble container. */
1237
1263
  backgroundColor?: string;
1264
+ /** Border color of the tool call bubble container. */
1238
1265
  borderColor?: string;
1266
+ /** Border width of the tool call bubble container (CSS value, e.g. `"1px"`). */
1239
1267
  borderWidth?: string;
1268
+ /** Border radius of the tool call bubble container (CSS value, e.g. `"12px"`). */
1240
1269
  borderRadius?: string;
1270
+ /** Background color of the collapsed header row. */
1241
1271
  headerBackgroundColor?: string;
1272
+ /** Text color of the collapsed header row (tool name / summary). */
1242
1273
  headerTextColor?: string;
1274
+ /** Horizontal padding of the collapsed header row (CSS value). */
1243
1275
  headerPaddingX?: string;
1276
+ /** Vertical padding of the collapsed header row (CSS value). */
1244
1277
  headerPaddingY?: string;
1278
+ /** Background color of the expanded content area. */
1245
1279
  contentBackgroundColor?: string;
1280
+ /** Text color of the expanded content area. */
1246
1281
  contentTextColor?: string;
1282
+ /** Horizontal padding of the expanded content area (CSS value). */
1247
1283
  contentPaddingX?: string;
1284
+ /** Vertical padding of the expanded content area (CSS value). */
1248
1285
  contentPaddingY?: string;
1286
+ /** Background color of code blocks (arguments / result) in the expanded area. */
1249
1287
  codeBlockBackgroundColor?: string;
1288
+ /** Border color of code blocks in the expanded area. */
1250
1289
  codeBlockBorderColor?: string;
1290
+ /** Text color of code blocks in the expanded area. */
1251
1291
  codeBlockTextColor?: string;
1292
+ /** Color of the expand/collapse toggle icon. */
1252
1293
  toggleTextColor?: string;
1294
+ /** Color of section labels ("Arguments", "Result", "Activity") in the expanded area. */
1253
1295
  labelTextColor?: string;
1254
1296
  /**
1255
1297
  * Override the collapsed summary row content for a tool call bubble.
@@ -1263,6 +1305,14 @@ export type AgentWidgetToolCallConfig = {
1263
1305
  collapsedMode: AgentWidgetToolCallCollapsedMode;
1264
1306
  isActive: boolean;
1265
1307
  config: AgentWidgetConfig;
1308
+ /** Static elapsed time snapshot, e.g. "2.6s". */
1309
+ elapsed: string;
1310
+ /**
1311
+ * Returns a `<span>` whose text content is automatically updated every
1312
+ * 100ms by the widget's global timer. Place it anywhere in your returned
1313
+ * HTMLElement to get a live-ticking duration display.
1314
+ */
1315
+ createElapsedElement: () => HTMLElement;
1266
1316
  }) => HTMLElement | string | null;
1267
1317
  /**
1268
1318
  * Override the lightweight collapsed preview content shown for active tool rows.
@@ -1285,6 +1335,47 @@ export type AgentWidgetToolCallConfig = {
1285
1335
  defaultSummary: string;
1286
1336
  config: AgentWidgetConfig;
1287
1337
  }) => HTMLElement | string | null;
1338
+ /**
1339
+ * Template string for the header text while a tool call is active (running).
1340
+ *
1341
+ * **Placeholders:** `{toolName}` (tool name), `{duration}` (live-updating elapsed time).
1342
+ *
1343
+ * **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — parsed at render time and
1344
+ * applied as styled `<span>` elements. Works with all animation modes.
1345
+ *
1346
+ * When not set, falls back to the current `collapsedMode` behavior.
1347
+ * @example "Calling {toolName}... ~{duration}~"
1348
+ * @example "**Searching** *{toolName}*..."
1349
+ */
1350
+ activeTextTemplate?: string;
1351
+ /**
1352
+ * Template string for the header text when a tool call is complete.
1353
+ *
1354
+ * **Placeholders:** `{toolName}` (tool name), `{duration}` (final elapsed time).
1355
+ *
1356
+ * **Inline formatting:** `~dim~`, `*italic*`, `**bold**` — same syntax as `activeTextTemplate`.
1357
+ *
1358
+ * When not set, falls back to the existing "Used tool for X seconds" text.
1359
+ * @example "Finished {toolName} ~{duration}~"
1360
+ */
1361
+ completeTextTemplate?: string;
1362
+ /**
1363
+ * Primary color for shimmer-color animation mode.
1364
+ * Defaults to the current text color.
1365
+ */
1366
+ loadingAnimationColor?: string;
1367
+ /**
1368
+ * Secondary/end color for shimmer-color animation mode.
1369
+ * Creates a gradient sweep between `loadingAnimationColor` and this color.
1370
+ * @default "#3b82f6"
1371
+ */
1372
+ loadingAnimationSecondaryColor?: string;
1373
+ /**
1374
+ * Duration of one full animation cycle in milliseconds.
1375
+ * Applies to pulse, shimmer, shimmer-color, and rainbow modes.
1376
+ * @default 2000
1377
+ */
1378
+ loadingAnimationDuration?: number;
1288
1379
  };
1289
1380
 
1290
1381
  export type AgentWidgetReasoningConfig = {
@@ -2081,6 +2172,35 @@ export type AgentWidgetAttachmentsConfig = {
2081
2172
  * Callback when a file is rejected (wrong type or too large).
2082
2173
  */
2083
2174
  onFileRejected?: (file: File, reason: 'type' | 'size' | 'count') => void;
2175
+ /**
2176
+ * Customize the drag-and-drop overlay that appears when files are dragged over the widget.
2177
+ */
2178
+ dropOverlay?: {
2179
+ /** Background color/value of the overlay. @default 'rgba(59, 130, 246, 0.08)' */
2180
+ background?: string;
2181
+ /** Backdrop blur applied behind the overlay (CSS value). @default '8px' */
2182
+ backdropBlur?: string;
2183
+ /** Border style shown during drag. @default '2px dashed rgba(59, 130, 246, 0.4)' */
2184
+ border?: string;
2185
+ /** Border radius of the overlay. @default 'inherit' */
2186
+ borderRadius?: string;
2187
+ /** Inset/margin pulling the overlay away from the container edges (CSS value). @default '0' */
2188
+ inset?: string;
2189
+ /** Lucide icon name displayed in the center. @default 'upload' */
2190
+ iconName?: string;
2191
+ /** Icon size (CSS value). @default '48px' */
2192
+ iconSize?: string;
2193
+ /** Icon stroke color. @default 'rgba(59, 130, 246, 0.6)' */
2194
+ iconColor?: string;
2195
+ /** Icon stroke width. @default 0.5 */
2196
+ iconStrokeWidth?: number;
2197
+ /** Optional label text shown below the icon. */
2198
+ label?: string;
2199
+ /** Label font size. @default '0.875rem' */
2200
+ labelSize?: string;
2201
+ /** Label color. @default 'rgba(59, 130, 246, 0.8)' */
2202
+ labelColor?: string;
2203
+ };
2084
2204
  };
2085
2205
 
2086
2206
  /**
@@ -0,0 +1,188 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+ import { AttachmentManager } from "./utils/attachment-manager";
7
+
8
+ const createMount = () => {
9
+ const mount = document.createElement("div");
10
+ document.body.appendChild(mount);
11
+ return mount;
12
+ };
13
+
14
+ /** jsdom does not expose `DataTransfer`; real browsers set `dropEffect` on dragover. */
15
+ function createFileDataTransfer(files: File[]): DataTransfer {
16
+ const list: File[] = [...files];
17
+ const fileList = list as unknown as FileList;
18
+ return {
19
+ dropEffect: "none",
20
+ effectAllowed: "all",
21
+ files: fileList,
22
+ items: {
23
+ add: () => {},
24
+ clear: () => {},
25
+ remove: () => {}
26
+ } as unknown as DataTransferItemList,
27
+ types: files.length > 0 ? ["Files"] : [],
28
+ clearData: () => {},
29
+ getData: () => "",
30
+ setData: () => {},
31
+ setDragImage: () => {}
32
+ } as unknown as DataTransfer;
33
+ }
34
+
35
+ function createDragEvent(type: string, dataTransfer: DataTransfer): DragEvent {
36
+ const ev = new Event(type, { bubbles: true, cancelable: true }) as unknown as DragEvent;
37
+ Object.defineProperty(ev, "dataTransfer", { value: dataTransfer, enumerable: true });
38
+ return ev;
39
+ }
40
+
41
+ describe("createAgentExperience attachment file drop", () => {
42
+ beforeEach(() => {
43
+ vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
44
+ cb(0);
45
+ return 1;
46
+ });
47
+ vi.stubGlobal("cancelAnimationFrame", () => {});
48
+ window.scrollTo = vi.fn();
49
+ });
50
+
51
+ afterEach(() => {
52
+ document.body.innerHTML = "";
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ it("calls AttachmentManager.handleFiles when files are dropped on the mount", () => {
57
+ const handleFilesSpy = vi.spyOn(AttachmentManager.prototype, "handleFiles");
58
+
59
+ const mount = createMount();
60
+ const controller = createAgentExperience(mount, {
61
+ apiUrl: "https://api.example.com/chat",
62
+ launcher: { enabled: false },
63
+ attachments: { enabled: true, maxFiles: 4 },
64
+ });
65
+
66
+ const file = new File(["x"], "test.png", { type: "image/png" });
67
+ const dt = createFileDataTransfer([file]);
68
+
69
+ // dragover/drop are on mount so the browser default is suppressed everywhere
70
+ const dragOver = createDragEvent("dragover", dt);
71
+ mount.dispatchEvent(dragOver);
72
+ expect(dragOver.defaultPrevented).toBe(true);
73
+ expect(dt.dropEffect).toBe("copy");
74
+
75
+ const drop = createDragEvent("drop", dt);
76
+ mount.dispatchEvent(drop);
77
+ expect(drop.defaultPrevented).toBe(true);
78
+
79
+ expect(handleFilesSpy).toHaveBeenCalledTimes(1);
80
+ const passed = handleFilesSpy.mock.calls[0]?.[0] as File[];
81
+ expect(passed).toHaveLength(1);
82
+ expect(passed[0]?.name).toBe("test.png");
83
+
84
+ handleFilesSpy.mockRestore();
85
+ controller.destroy();
86
+ });
87
+
88
+ it("shows drop-active highlight on container during dragenter", () => {
89
+ const mount = createMount();
90
+ const controller = createAgentExperience(mount, {
91
+ apiUrl: "https://api.example.com/chat",
92
+ launcher: { enabled: false },
93
+ attachments: { enabled: true, maxFiles: 4 },
94
+ });
95
+
96
+ const container = mount.querySelector(".persona-widget-container")!;
97
+ const file = new File(["x"], "test.png", { type: "image/png" });
98
+ const dt = createFileDataTransfer([file]);
99
+
100
+ container.dispatchEvent(createDragEvent("dragenter", dt));
101
+ expect(container.classList.contains("persona-attachment-drop-active")).toBe(true);
102
+
103
+ container.dispatchEvent(createDragEvent("dragleave", dt));
104
+ expect(container.classList.contains("persona-attachment-drop-active")).toBe(false);
105
+
106
+ controller.destroy();
107
+ });
108
+
109
+ it("renders drop overlay with icon inside container", () => {
110
+ const mount = createMount();
111
+ const controller = createAgentExperience(mount, {
112
+ apiUrl: "https://api.example.com/chat",
113
+ launcher: { enabled: false },
114
+ attachments: { enabled: true, maxFiles: 4 },
115
+ });
116
+
117
+ const overlay = mount.querySelector(".persona-attachment-drop-overlay");
118
+ expect(overlay).not.toBeNull();
119
+ expect(overlay!.querySelector("svg")).not.toBeNull();
120
+
121
+ controller.destroy();
122
+ });
123
+
124
+ it("applies custom dropOverlay config as CSS variables", () => {
125
+ const mount = createMount();
126
+ const controller = createAgentExperience(mount, {
127
+ apiUrl: "https://api.example.com/chat",
128
+ launcher: { enabled: false },
129
+ attachments: {
130
+ enabled: true,
131
+ maxFiles: 4,
132
+ dropOverlay: {
133
+ background: "rgba(255, 0, 0, 0.1)",
134
+ backdropBlur: "12px",
135
+ border: "2px solid red",
136
+ inset: "8px",
137
+ iconName: "image-plus",
138
+ label: "Drop here",
139
+ },
140
+ },
141
+ });
142
+
143
+ const overlay = mount.querySelector<HTMLElement>(".persona-attachment-drop-overlay")!;
144
+ expect(overlay).not.toBeNull();
145
+ expect(overlay.style.getPropertyValue("--persona-drop-overlay-bg")).toBe("rgba(255, 0, 0, 0.1)");
146
+ expect(overlay.style.getPropertyValue("--persona-drop-overlay-blur")).toBe("12px");
147
+ expect(overlay.style.getPropertyValue("--persona-drop-overlay-border")).toBe("2px solid red");
148
+ expect(overlay.style.getPropertyValue("--persona-drop-overlay-inset")).toBe("8px");
149
+
150
+ const label = overlay.querySelector(".persona-drop-overlay-label");
151
+ expect(label).not.toBeNull();
152
+ expect(label!.textContent).toBe("Drop here");
153
+
154
+ controller.destroy();
155
+ });
156
+
157
+ it("does not render drop overlay when attachments are disabled", () => {
158
+ const mount = createMount();
159
+ const controller = createAgentExperience(mount, {
160
+ apiUrl: "https://api.example.com/chat",
161
+ launcher: { enabled: false },
162
+ attachments: { enabled: false },
163
+ });
164
+
165
+ const overlay = mount.querySelector(".persona-attachment-drop-overlay");
166
+ expect(overlay).toBeNull();
167
+
168
+ controller.destroy();
169
+ });
170
+
171
+ it("does not prevent dragover when attachments are disabled", () => {
172
+ const mount = createMount();
173
+ const controller = createAgentExperience(mount, {
174
+ apiUrl: "https://api.example.com/chat",
175
+ launcher: { enabled: false },
176
+ attachments: { enabled: false },
177
+ });
178
+
179
+ const file = new File(["x"], "test.png", { type: "image/png" });
180
+ const dt = createFileDataTransfer([file]);
181
+
182
+ const dragOver = createDragEvent("dragover", dt);
183
+ mount.dispatchEvent(dragOver);
184
+ expect(dragOver.defaultPrevented).toBe(false);
185
+
186
+ controller.destroy();
187
+ });
188
+ });
@@ -34,6 +34,16 @@ const installRafMock = () => {
34
34
  });
35
35
 
36
36
  return {
37
+ step(frameCount = 1) {
38
+ let frames = 0;
39
+ while (callbacks.size > 0 && frames < frameCount) {
40
+ const pending = [...callbacks.entries()];
41
+ callbacks.clear();
42
+ frames += 1;
43
+ now += 16;
44
+ pending.forEach(([, callback]) => callback(now));
45
+ }
46
+ },
37
47
  flush(maxFrames = 80) {
38
48
  let frames = 0;
39
49
  while (callbacks.size > 0 && frames < maxFrames) {
@@ -215,14 +225,14 @@ describe("createAgentExperience streaming scroll", () => {
215
225
 
216
226
  expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
217
227
 
218
- metrics.setScrollTop(metrics.getBottomScrollTop() - 3);
228
+ metrics.setScrollTop(metrics.getBottomScrollTop() - 6);
219
229
  scrollContainer!.dispatchEvent(new Event("scroll"));
220
230
 
221
231
  metrics.setScrollHeight(1040);
222
232
  emitStreamingMessage(controller, "Second chunk");
223
233
  raf.flush();
224
234
 
225
- expect(metrics.getScrollTop()).toBe(597);
235
+ expect(metrics.getScrollTop()).toBe(594);
226
236
 
227
237
  controller.destroy();
228
238
  });
@@ -364,6 +374,39 @@ describe("createAgentExperience streaming scroll", () => {
364
374
  controller.destroy();
365
375
  });
366
376
 
377
+ it("catches up immediately when a streamed update lands far behind", () => {
378
+ const raf = installRafMock();
379
+ const mount = createMount();
380
+ const controller = createAgentExperience(mount, {
381
+ apiUrl: "https://api.example.com/chat",
382
+ launcher: { enabled: false }
383
+ });
384
+
385
+ const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
386
+ expect(scrollContainer).not.toBeNull();
387
+
388
+ const metrics = installScrollMetrics(scrollContainer!, {
389
+ scrollHeight: 900,
390
+ clientHeight: 400
391
+ });
392
+
393
+ emitStreamingStatus(controller);
394
+ emitStreamingMessage(controller, "Chunk one");
395
+ raf.flush();
396
+
397
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
398
+
399
+ metrics.setScrollHeight(1080);
400
+ emitStreamingMessage(controller, "Chunk two");
401
+
402
+ // Only run the scheduled auto-scroll frame, not the whole animation.
403
+ raf.step(1);
404
+
405
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
406
+
407
+ controller.destroy();
408
+ });
409
+
367
410
  it("lets the user break away during reasoning streaming", () => {
368
411
  const raf = installRafMock();
369
412
  const mount = createMount();
@@ -472,6 +515,52 @@ describe("createAgentExperience streaming scroll", () => {
472
515
  controller.destroy();
473
516
  });
474
517
 
518
+ it("ignores layout-driven scroll events before a scheduled auto-scroll starts", () => {
519
+ const raf = installRafMock();
520
+ const mount = createMount();
521
+ const controller = createAgentExperience(mount, {
522
+ apiUrl: "https://api.example.com/chat",
523
+ launcher: { enabled: false },
524
+ features: {
525
+ toolCallDisplay: {
526
+ activePreview: true,
527
+ },
528
+ },
529
+ } as any);
530
+
531
+ const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
532
+ expect(scrollContainer).not.toBeNull();
533
+
534
+ const metrics = installScrollMetrics(scrollContainer!, {
535
+ scrollHeight: 960,
536
+ clientHeight: 400,
537
+ });
538
+
539
+ emitStreamingStatus(controller);
540
+ emitToolMessage(controller, { id: "tool-1", chunks: ["Loaded tools"] });
541
+ emitToolMessage(controller, { id: "tool-2", chunks: ["Fetched docs"] });
542
+ raf.flush();
543
+
544
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
545
+
546
+ metrics.setScrollHeight(1035);
547
+ emitToolMessage(controller, {
548
+ id: "tool-3",
549
+ chunks: ["Compared layouts and noted launcher sizing"],
550
+ });
551
+
552
+ // Simulate the browser emitting a scroll event caused by layout/scroll
553
+ // anchoring before the scheduled auto-scroll rAF has started.
554
+ metrics.setScrollTop(metrics.getScrollTop() - 2);
555
+ scrollContainer!.dispatchEvent(new Event("scroll"));
556
+
557
+ raf.flush();
558
+
559
+ expect(metrics.getScrollTop()).toBe(metrics.getBottomScrollTop());
560
+
561
+ controller.destroy();
562
+ });
563
+
475
564
  it("uses icon-only arrow-down defaults for the transcript affordance", () => {
476
565
  const raf = installRafMock();
477
566
  const mount = createMount();