@runtypelabs/persona 4.15.0 → 4.17.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.
Files changed (91) hide show
  1. package/dist/codegen.cjs +1 -1
  2. package/dist/codegen.js +1 -1
  3. package/dist/context-mentions.d.cts +283 -9
  4. package/dist/context-mentions.d.ts +283 -9
  5. package/dist/index.cjs +68 -68
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +299 -10
  8. package/dist/index.d.ts +299 -10
  9. package/dist/index.global.js +57 -57
  10. package/dist/index.global.js.map +1 -1
  11. package/dist/index.js +69 -69
  12. package/dist/index.js.map +1 -1
  13. package/dist/install.global.js +1 -1
  14. package/dist/install.global.js.map +1 -1
  15. package/dist/launcher.global.js +2 -2
  16. package/dist/launcher.global.js.map +1 -1
  17. package/dist/markdown-parsers.js +36 -36
  18. package/dist/smart-dom-reader.d.cts +283 -9
  19. package/dist/smart-dom-reader.d.ts +283 -9
  20. package/dist/testing.cjs +3 -3
  21. package/dist/testing.d.cts +7 -0
  22. package/dist/testing.d.ts +7 -0
  23. package/dist/testing.js +3 -3
  24. package/dist/theme-editor-preview.cjs +64 -64
  25. package/dist/theme-editor-preview.d.cts +283 -9
  26. package/dist/theme-editor-preview.d.ts +283 -9
  27. package/dist/theme-editor-preview.js +56 -56
  28. package/dist/theme-editor.cjs +10 -10
  29. package/dist/theme-editor.d.cts +283 -10
  30. package/dist/theme-editor.d.ts +283 -10
  31. package/dist/theme-editor.js +10 -10
  32. package/dist/theme-reference.cjs +1 -1
  33. package/dist/theme-reference.d.cts +66 -3
  34. package/dist/theme-reference.d.ts +66 -3
  35. package/dist/theme-reference.js +1 -1
  36. package/dist/widget.css +1 -1
  37. package/package.json +2 -2
  38. package/src/components/artifact-pane.test.ts +27 -0
  39. package/src/components/artifact-pane.ts +7 -3
  40. package/src/components/header-builder.ts +27 -2
  41. package/src/components/header-layouts.test.ts +56 -0
  42. package/src/components/header-layouts.ts +29 -9
  43. package/src/components/launcher-surface.test.ts +403 -0
  44. package/src/components/launcher.ts +297 -2
  45. package/src/components/panel.ts +69 -74
  46. package/src/components/suggestions.test.ts +40 -0
  47. package/src/components/suggestions.ts +213 -153
  48. package/src/components/welcome.test.ts +121 -0
  49. package/src/components/welcome.ts +305 -0
  50. package/src/components/widget-view.test.ts +5 -3
  51. package/src/components/widget-view.ts +14 -7
  52. package/src/defaults.ts +2 -2
  53. package/src/index-core.ts +12 -1
  54. package/src/install.test.ts +35 -1
  55. package/src/install.ts +47 -11
  56. package/src/launcher-global.ts +10 -7
  57. package/src/plugins/types.ts +66 -1
  58. package/src/styles/widget-styles.test.ts +220 -0
  59. package/src/styles/widget.css +353 -9
  60. package/src/testing/mock-stream.test.ts +19 -0
  61. package/src/testing/mock-stream.ts +20 -0
  62. package/src/theme-editor/sections.ts +30 -1
  63. package/src/theme-reference.ts +26 -4
  64. package/src/types/theme.ts +40 -3
  65. package/src/types.ts +164 -6
  66. package/src/ui.body-top-slot.test.ts +95 -0
  67. package/src/ui.content-width.test.ts +91 -0
  68. package/src/ui.docked.test.ts +7 -4
  69. package/src/ui.launcher-teaser.test.ts +162 -0
  70. package/src/ui.rebuild-composer.test.ts +491 -0
  71. package/src/ui.render-welcome.test.ts +505 -0
  72. package/src/ui.scroll-additive.test.ts +91 -21
  73. package/src/ui.scroll.test.ts +302 -14
  74. package/src/ui.suggest-replies.test.ts +46 -0
  75. package/src/ui.ts +747 -136
  76. package/src/ui.welcome.test.ts +504 -0
  77. package/src/utils/artifact-gate.test.ts +27 -0
  78. package/src/utils/artifact-gate.ts +9 -0
  79. package/src/utils/attachment-manager.test.ts +37 -0
  80. package/src/utils/attachment-manager.ts +15 -0
  81. package/src/utils/auto-follow.test.ts +23 -0
  82. package/src/utils/auto-follow.ts +11 -1
  83. package/src/utils/config-merge.ts +2 -0
  84. package/src/utils/content-width.test.ts +37 -0
  85. package/src/utils/content-width.ts +17 -0
  86. package/src/utils/plugin-storage.test.ts +94 -0
  87. package/src/utils/plugin-storage.ts +98 -0
  88. package/src/utils/theme.test.ts +137 -0
  89. package/src/utils/tokens.ts +49 -3
  90. package/src/welcome.test.ts +213 -0
  91. package/src/welcome.ts +130 -0
@@ -0,0 +1,505 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+ import type { AgentWidgetPlugin } from "./plugins/types";
7
+ import type { AgentWidgetMessage } from "./types";
8
+
9
+ const mounts: HTMLElement[] = [];
10
+ const controllers: ReturnType<typeof createAgentExperience>[] = [];
11
+
12
+ const makeController = (config: Record<string, unknown> = {}) => {
13
+ const mount = document.createElement("div");
14
+ document.body.appendChild(mount);
15
+ mounts.push(mount);
16
+ const controller = createAgentExperience(mount, {
17
+ apiUrl: "https://api.example.com/chat",
18
+ launcher: { enabled: false },
19
+ persistState: false,
20
+ suggestionChips: [],
21
+ ...config,
22
+ } as unknown as Parameters<typeof createAgentExperience>[1]);
23
+ controllers.push(controller);
24
+ return { mount, controller };
25
+ };
26
+
27
+ const host = (mount: HTMLElement) =>
28
+ mount.querySelector<HTMLElement>("[data-persona-welcome]")!;
29
+
30
+ const userMessage = (id = "u1"): AgentWidgetMessage => ({
31
+ id,
32
+ role: "user",
33
+ content: "hello",
34
+ createdAt: "2026-08-01T00:00:00.000Z",
35
+ streaming: false,
36
+ });
37
+
38
+ describe("renderWelcome plugin hook", () => {
39
+ afterEach(() => {
40
+ controllers.splice(0).forEach((controller) => controller.destroy());
41
+ mounts.splice(0).forEach((mount) => mount.remove());
42
+ document.body.innerHTML = "";
43
+ window.localStorage.clear();
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it("renders the plugin element inside the core-owned host and hides the default content", () => {
48
+ const plugin: AgentWidgetPlugin = {
49
+ id: "home",
50
+ renderWelcome: () => {
51
+ const root = document.createElement("div");
52
+ root.setAttribute("data-test-home", "");
53
+ return root;
54
+ },
55
+ };
56
+ const { mount } = makeController({ plugins: [plugin] });
57
+
58
+ const welcome = host(mount);
59
+ const content = welcome.querySelector("[data-test-home]");
60
+ expect(content).not.toBeNull();
61
+ expect(content!.hasAttribute("data-persona-welcome-plugin")).toBe(true);
62
+ expect(welcome.getAttribute("data-persona-welcome-content")).toBe("plugin");
63
+ // The host itself is never removed: it is the permanent mount point.
64
+ expect(welcome.isConnected).toBe(true);
65
+ });
66
+
67
+ it("first plugin returning an element wins; null falls through", () => {
68
+ const order: string[] = [];
69
+ const skipping: AgentWidgetPlugin = {
70
+ id: "skips",
71
+ priority: 10,
72
+ renderWelcome: () => {
73
+ order.push("skips");
74
+ return null;
75
+ },
76
+ };
77
+ const winning: AgentWidgetPlugin = {
78
+ id: "wins",
79
+ priority: 5,
80
+ renderWelcome: () => {
81
+ order.push("wins");
82
+ const root = document.createElement("div");
83
+ root.setAttribute("data-test-owner", "wins");
84
+ return root;
85
+ },
86
+ };
87
+ const never: AgentWidgetPlugin = {
88
+ id: "never",
89
+ priority: 1,
90
+ renderWelcome: () => {
91
+ order.push("never");
92
+ const root = document.createElement("div");
93
+ root.setAttribute("data-test-owner", "never");
94
+ return root;
95
+ },
96
+ };
97
+ const { mount } = makeController({ plugins: [winning, skipping, never] });
98
+
99
+ expect(order).toEqual(["skips", "wins"]);
100
+ expect(
101
+ host(mount).querySelector("[data-test-owner]")!.getAttribute("data-test-owner")
102
+ ).toBe("wins");
103
+ });
104
+
105
+ it("falls through to the default renderer when every plugin returns null", () => {
106
+ const plugin: AgentWidgetPlugin = { id: "noop", renderWelcome: () => null };
107
+ const { mount } = makeController({
108
+ plugins: [plugin],
109
+ welcome: { title: "Hi there" },
110
+ });
111
+
112
+ const welcome = host(mount);
113
+ expect(welcome.hasAttribute("data-persona-welcome-content")).toBe(false);
114
+ expect(welcome.hasAttribute("data-persona-welcome-overlay")).toBe(false);
115
+ expect(welcome.textContent).toContain("Hi there");
116
+ });
117
+
118
+ it("passes the alias-resolved config, variant, and derived visibility", () => {
119
+ const seen: Array<Record<string, unknown>> = [];
120
+ const plugin: AgentWidgetPlugin = {
121
+ id: "probe",
122
+ renderWelcome: (ctx) => {
123
+ seen.push({
124
+ title: ctx.config.title,
125
+ subtitle: ctx.config.subtitle,
126
+ variant: ctx.variant,
127
+ visible: ctx.visible,
128
+ });
129
+ return null;
130
+ },
131
+ };
132
+ makeController({
133
+ plugins: [plugin],
134
+ copy: { welcomeTitle: "Legacy title" },
135
+ welcome: { subtitle: "Scoped help", variant: "hero" },
136
+ });
137
+
138
+ expect(seen[0]).toEqual({
139
+ title: "Legacy title",
140
+ subtitle: "Scoped help",
141
+ variant: "hero",
142
+ visible: true,
143
+ });
144
+ });
145
+
146
+ it("renders plugin content regardless of derived visibility and overlays the transcript", () => {
147
+ const plugin: AgentWidgetPlugin = {
148
+ id: "home",
149
+ renderWelcome: () => {
150
+ const root = document.createElement("div");
151
+ root.setAttribute("data-test-home", "");
152
+ return root;
153
+ },
154
+ };
155
+ const { mount, controller } = makeController({
156
+ plugins: [plugin],
157
+ welcome: { variant: "hero" },
158
+ });
159
+
160
+ controller.injectUserMessage({ content: "book a table" });
161
+
162
+ const welcome = host(mount);
163
+ // Derived visibility is false after a user message; the plugin element wins.
164
+ expect(welcome.hidden).toBe(false);
165
+ expect(welcome.style.display).not.toBe("none");
166
+ expect(welcome.hasAttribute("data-persona-welcome-overlay")).toBe(true);
167
+ const body = mount.querySelector<HTMLElement>("#persona-scroll-container")!;
168
+ expect(body.classList.contains("persona-welcome-overlay-active")).toBe(true);
169
+ });
170
+
171
+ it("requestRender re-arbitrates: cleanups run, previous content is removed, overlay clears", () => {
172
+ const cleanup = vi.fn();
173
+ let showHome = true;
174
+ let request: () => void = () => {};
175
+ const plugin: AgentWidgetPlugin = {
176
+ id: "home",
177
+ renderWelcome: (ctx) => {
178
+ request = ctx.requestRender;
179
+ ctx.onCleanup(cleanup);
180
+ if (!showHome) return null;
181
+ const root = document.createElement("div");
182
+ root.setAttribute("data-test-home", "");
183
+ return root;
184
+ },
185
+ };
186
+ const { mount } = makeController({ plugins: [plugin] });
187
+
188
+ expect(host(mount).querySelectorAll("[data-test-home]").length).toBe(1);
189
+
190
+ showHome = false;
191
+ request();
192
+
193
+ expect(cleanup).toHaveBeenCalledTimes(1);
194
+ const welcome = host(mount);
195
+ expect(welcome.querySelector("[data-test-home]")).toBeNull();
196
+ expect(welcome.hasAttribute("data-persona-welcome-overlay")).toBe(false);
197
+ expect(
198
+ mount
199
+ .querySelector<HTMLElement>("#persona-scroll-container")!
200
+ .classList.contains("persona-welcome-overlay-active")
201
+ ).toBe(false);
202
+ });
203
+
204
+ it("re-shows plugin content over an existing transcript (home button flow)", () => {
205
+ let showHome = false;
206
+ let request: () => void = () => {};
207
+ const plugin: AgentWidgetPlugin = {
208
+ id: "home",
209
+ renderWelcome: (ctx) => {
210
+ request = ctx.requestRender;
211
+ if (!showHome) return null;
212
+ const root = document.createElement("div");
213
+ root.setAttribute("data-test-home", "");
214
+ return root;
215
+ },
216
+ };
217
+ const { mount, controller } = makeController({ plugins: [plugin] });
218
+
219
+ controller.injectUserMessage({ content: "hello" });
220
+ showHome = true;
221
+ request();
222
+
223
+ const welcome = host(mount);
224
+ expect(welcome.querySelector("[data-test-home]")).not.toBeNull();
225
+ expect(welcome.hasAttribute("data-persona-welcome-overlay")).toBe(true);
226
+ });
227
+
228
+ it("zeroes the body scroll while the overlay is active and restores it after", () => {
229
+ // An absolute inset: 0 child of a scrolled scroller maps to the content
230
+ // origin, not the viewport; without the reset the overlay paints
231
+ // scrollTop pixels above the visible area.
232
+ let showHome = false;
233
+ let request: () => void = () => {};
234
+ const plugin: AgentWidgetPlugin = {
235
+ id: "home",
236
+ renderWelcome: (ctx) => {
237
+ request = ctx.requestRender;
238
+ if (!showHome) return null;
239
+ const root = document.createElement("div");
240
+ root.setAttribute("data-test-home", "");
241
+ return root;
242
+ },
243
+ };
244
+ const { mount, controller } = makeController({ plugins: [plugin] });
245
+
246
+ controller.injectUserMessage({ content: "hello" });
247
+ const body = mount.querySelector<HTMLElement>("#persona-scroll-container")!;
248
+ body.scrollTop = 240;
249
+
250
+ showHome = true;
251
+ request();
252
+ expect(body.scrollTop).toBe(0);
253
+
254
+ showHome = false;
255
+ request();
256
+ expect(body.scrollTop).toBe(240);
257
+ });
258
+
259
+ it("re-enables renderStarter output when a stream ends", async () => {
260
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
261
+ const originalFetch = global.fetch;
262
+ global.fetch = vi.fn().mockImplementation((_url: string, options: { signal: AbortSignal }) => {
263
+ const signal = options.signal;
264
+ return new Promise((_resolve, reject) => {
265
+ signal.addEventListener("abort", () => {
266
+ const error = new Error("aborted");
267
+ error.name = "AbortError";
268
+ reject(error);
269
+ });
270
+ });
271
+ }) as unknown as typeof fetch;
272
+
273
+ try {
274
+ let request: () => void = () => {};
275
+ const plugin: AgentWidgetPlugin = {
276
+ id: "home",
277
+ renderWelcome: (ctx) => {
278
+ request = ctx.requestRender;
279
+ const root = document.createElement("div");
280
+ root.appendChild(ctx.renderStarter({ label: "Track my order" }));
281
+ return root;
282
+ },
283
+ };
284
+ const { mount } = makeController({ plugins: [plugin] });
285
+
286
+ const textarea = mount.querySelector<HTMLTextAreaElement>(
287
+ "[data-persona-composer-input]"
288
+ )!;
289
+ textarea.value = "start a stream";
290
+ const form = mount.querySelector<HTMLFormElement>(
291
+ "[data-persona-composer-form]"
292
+ )!;
293
+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
294
+ await flush();
295
+
296
+ // Rebuild the plugin stack mid-stream: the starter is born disabled.
297
+ request();
298
+ const starter = host(mount).querySelector<HTMLButtonElement>(
299
+ ".persona-suggestion"
300
+ )!;
301
+ expect(starter.disabled).toBe(true);
302
+
303
+ // Submitting while streaming is the stop path; the one-shot starter
304
+ // DOM must be re-enabled with the managed surfaces.
305
+ form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
306
+ await flush();
307
+ expect(starter.disabled).toBe(false);
308
+ } finally {
309
+ global.fetch = originalFetch;
310
+ }
311
+ });
312
+
313
+ it("composes with defaultRenderer() without taking over visibility", () => {
314
+ const plugin: AgentWidgetPlugin = {
315
+ id: "search",
316
+ renderWelcome: ({ defaultRenderer, onCleanup }) => {
317
+ const card = defaultRenderer();
318
+ const search = document.createElement("input");
319
+ search.setAttribute("data-test-search", "");
320
+ card.appendChild(search);
321
+ onCleanup(() => search.remove());
322
+ return card;
323
+ },
324
+ };
325
+ const { mount, controller } = makeController({
326
+ plugins: [plugin],
327
+ welcome: { variant: "hero" },
328
+ });
329
+
330
+ const welcome = host(mount);
331
+ expect(welcome.querySelector("[data-test-search]")).not.toBeNull();
332
+ // Composition is not a takeover: no overlay, derived visibility still rules.
333
+ expect(welcome.hasAttribute("data-persona-welcome-overlay")).toBe(false);
334
+
335
+ controller.injectUserMessage({ content: "hi" });
336
+ expect(welcome.hidden || welcome.style.display === "none").toBe(true);
337
+ });
338
+
339
+ it("renderStarter runs the full select pipeline (events, hooks, send)", () => {
340
+ const selected: string[] = [];
341
+ const plugin: AgentWidgetPlugin = {
342
+ id: "home",
343
+ onSuggestionSelect: ({ suggestion }) => {
344
+ selected.push(suggestion.prompt);
345
+ },
346
+ renderWelcome: ({ renderStarter }) => {
347
+ const root = document.createElement("div");
348
+ root.setAttribute("data-test-home", "");
349
+ root.appendChild(
350
+ renderStarter({ label: "Track my order", prompt: "Where is my order?" })
351
+ );
352
+ return root;
353
+ },
354
+ };
355
+ const { mount, controller } = makeController({ plugins: [plugin] });
356
+
357
+ const shown = vi.fn();
358
+ mount.addEventListener("persona:suggestion:selected", shown);
359
+
360
+ const starter = host(mount).querySelector<HTMLButtonElement>(
361
+ "button.persona-suggestion"
362
+ )!;
363
+ expect(starter.dataset.suggestionId).toBe("Where is my order?");
364
+ starter.click();
365
+
366
+ expect(shown).toHaveBeenCalledTimes(1);
367
+ expect(selected).toEqual(["Where is my order?"]);
368
+ expect(
369
+ controller.getMessages().some((m) => m.content === "Where is my order?")
370
+ ).toBe(true);
371
+ });
372
+
373
+ it("renderStarter honors a canceled persona:suggestion:selected event", () => {
374
+ const plugin: AgentWidgetPlugin = {
375
+ id: "home",
376
+ renderWelcome: ({ renderStarter }) => {
377
+ const root = document.createElement("div");
378
+ root.appendChild(renderStarter("Ask about pricing"));
379
+ return root;
380
+ },
381
+ };
382
+ const { mount, controller } = makeController({ plugins: [plugin] });
383
+ mount.addEventListener("persona:suggestion:selected", (event) =>
384
+ event.preventDefault()
385
+ );
386
+
387
+ host(mount).querySelector<HTMLButtonElement>("button.persona-suggestion")!.click();
388
+
389
+ expect(controller.getMessages()).toHaveLength(0);
390
+ });
391
+
392
+ it("renderStarter fills the composer when behavior is fill", () => {
393
+ const plugin: AgentWidgetPlugin = {
394
+ id: "home",
395
+ renderWelcome: ({ renderStarter }) => {
396
+ const root = document.createElement("div");
397
+ root.appendChild(
398
+ renderStarter({ label: "Draft a reply", behavior: "fill" })
399
+ );
400
+ return root;
401
+ },
402
+ };
403
+ const { mount, controller } = makeController({ plugins: [plugin] });
404
+
405
+ host(mount).querySelector<HTMLButtonElement>("button.persona-suggestion")!.click();
406
+
407
+ const textarea = mount.querySelector<HTMLTextAreaElement>(
408
+ "[data-persona-composer-input]"
409
+ )!;
410
+ expect(textarea.value).toBe("Draft a reply");
411
+ expect(controller.getMessages()).toHaveLength(0);
412
+ });
413
+
414
+ it("ctx.sendMessage posts a user message", () => {
415
+ const plugin: AgentWidgetPlugin = {
416
+ id: "home",
417
+ renderWelcome: ({ sendMessage }) => {
418
+ const root = document.createElement("div");
419
+ const button = document.createElement("button");
420
+ button.setAttribute("data-test-send", "");
421
+ button.addEventListener("click", () => sendMessage("start over"));
422
+ root.appendChild(button);
423
+ return root;
424
+ },
425
+ };
426
+ const { mount, controller } = makeController({ plugins: [plugin] });
427
+
428
+ host(mount).querySelector<HTMLButtonElement>("[data-test-send]")!.click();
429
+
430
+ expect(controller.getMessages().some((m) => m.content === "start over")).toBe(
431
+ true
432
+ );
433
+ });
434
+
435
+ it("ctx.storage persists across a re-render and namespaces by plugin id", () => {
436
+ const reads: Array<string | null> = [];
437
+ let request: () => void = () => {};
438
+ const plugin: AgentWidgetPlugin = {
439
+ id: "home",
440
+ renderWelcome: (ctx) => {
441
+ request = ctx.requestRender;
442
+ reads.push(ctx.storage.get("view"));
443
+ ctx.storage.set("view", "chat");
444
+ return null;
445
+ },
446
+ };
447
+ makeController({ plugins: [plugin], persistState: true });
448
+
449
+ request();
450
+
451
+ expect(reads).toEqual([null, "chat"]);
452
+ expect(window.localStorage.getItem("persona-plugin:home:view")).toBe("chat");
453
+ });
454
+
455
+ it("re-arbitrates when controller.update() changes the welcome config", () => {
456
+ const titles: string[] = [];
457
+ const plugin: AgentWidgetPlugin = {
458
+ id: "probe",
459
+ renderWelcome: (ctx) => {
460
+ titles.push(ctx.config.title);
461
+ return null;
462
+ },
463
+ };
464
+ const { controller } = makeController({
465
+ plugins: [plugin],
466
+ welcome: { title: "First" },
467
+ });
468
+
469
+ controller.update({ welcome: { title: "Second" } });
470
+ // An unrelated update must not churn plugin-owned content.
471
+ controller.update({ copy: { inputPlaceholder: "Type here" } });
472
+
473
+ expect(titles).toEqual(["First", "Second"]);
474
+ });
475
+
476
+ it("runs cleanups on destroy", () => {
477
+ const cleanup = vi.fn();
478
+ const plugin: AgentWidgetPlugin = {
479
+ id: "home",
480
+ renderWelcome: (ctx) => {
481
+ ctx.onCleanup(cleanup);
482
+ return null;
483
+ },
484
+ };
485
+ const { controller } = makeController({ plugins: [plugin] });
486
+
487
+ controllers.splice(controllers.indexOf(controller), 1);
488
+ controller.destroy();
489
+
490
+ expect(cleanup).toHaveBeenCalledTimes(1);
491
+ });
492
+
493
+ it("keeps derived dismissal for the default renderer when no plugin claims the surface", () => {
494
+ const plugin: AgentWidgetPlugin = { id: "noop", renderWelcome: () => null };
495
+ const { mount, controller } = makeController({
496
+ plugins: [plugin],
497
+ welcome: { variant: "hero" },
498
+ initialMessages: [userMessage()],
499
+ });
500
+
501
+ expect(host(mount).hidden || host(mount).style.display === "none").toBe(true);
502
+ controller.destroy();
503
+ controllers.splice(controllers.indexOf(controller), 1);
504
+ });
505
+ });