@copilotkit/vue 2.0.0-rc.0 → 2.0.0-rc.2

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 (239) hide show
  1. package/PARITY.md +13 -12
  2. package/README.md +1 -2
  3. package/dist/components/copilot-provider/CopilotKit.vue.d.ts +3 -1
  4. package/dist/components/copilot-provider/CopilotKit.vue.d.ts.map +1 -1
  5. package/dist/index.cjs +43 -46
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +10170 -7506
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/v2/components/A2UIMessageRenderer.d.ts +7 -0
  10. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  11. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  12. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +11 -2
  13. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +136 -0
  15. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -0
  16. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChat.vue.d.ts +1 -0
  18. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +3 -1
  20. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  21. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts +16 -0
  22. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts.map +1 -0
  23. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  26. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts +22 -0
  27. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -0
  28. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +69 -16
  29. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  30. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts +12 -0
  31. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts.map +1 -0
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts +22 -0
  33. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -0
  34. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +20 -0
  35. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatSuggestionPill.vue.d.ts.map +1 -1
  38. package/dist/v2/components/chat/CopilotChatSuggestionView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotChatToggleButton.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +32 -0
  41. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -0
  42. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +11 -1
  44. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  45. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -0
  47. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  48. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts +5 -0
  49. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -0
  50. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  51. package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
  52. package/dist/v2/components/chat/CopilotModalHeaderTitle.d.ts.map +1 -1
  53. package/dist/v2/components/chat/CopilotPopup.vue.d.ts +1 -0
  54. package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
  55. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts +1 -0
  56. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
  57. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts +1 -0
  58. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  59. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
  60. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts +2 -0
  61. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
  62. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +2 -0
  63. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
  64. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +2 -0
  65. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  66. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
  67. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  68. package/dist/v2/components/chat/index.d.ts +17 -0
  69. package/dist/v2/components/chat/index.d.ts.map +1 -1
  70. package/dist/v2/components/chat/inspector-shortcuts.d.ts +12 -0
  71. package/dist/v2/components/chat/inspector-shortcuts.d.ts.map +1 -0
  72. package/dist/v2/components/chat/message-feedback.d.ts +89 -0
  73. package/dist/v2/components/chat/message-feedback.d.ts.map +1 -0
  74. package/dist/v2/components/chat/tool-call-capture.d.ts +39 -0
  75. package/dist/v2/components/chat/tool-call-capture.d.ts.map +1 -0
  76. package/dist/v2/components/chat/tool-call-display-context.d.ts +39 -0
  77. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -0
  78. package/dist/v2/components/chat/tool-call-group-parts.d.ts +144 -0
  79. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -0
  80. package/dist/v2/components/chat/tool-call-layout.d.ts +115 -0
  81. package/dist/v2/components/chat/tool-call-layout.d.ts.map +1 -0
  82. package/dist/v2/components/chat/turn-work.d.ts +35 -0
  83. package/dist/v2/components/chat/turn-work.d.ts.map +1 -0
  84. package/dist/v2/components/chat/types.d.ts +104 -4
  85. package/dist/v2/components/chat/types.d.ts.map +1 -1
  86. package/dist/v2/components/chat/use-lightbox.d.ts +18 -0
  87. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -0
  88. package/dist/v2/components/icons/index.d.ts +1 -1
  89. package/dist/v2/components/icons/index.d.ts.map +1 -1
  90. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +55 -0
  91. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -0
  92. package/dist/v2/components/ui/CopilotPopover.vue.d.ts +40 -0
  93. package/dist/v2/components/ui/CopilotPopover.vue.d.ts.map +1 -0
  94. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts +37 -0
  95. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts.map +1 -0
  96. package/dist/v2/components/ui/dropdown-menu.d.ts +14 -0
  97. package/dist/v2/components/ui/dropdown-menu.d.ts.map +1 -0
  98. package/dist/v2/components/ui/floating.d.ts +84 -0
  99. package/dist/v2/components/ui/floating.d.ts.map +1 -0
  100. package/dist/v2/components/ui/presence.d.ts +9 -0
  101. package/dist/v2/components/ui/presence.d.ts.map +1 -0
  102. package/dist/v2/components/ui/use-id.d.ts +9 -0
  103. package/dist/v2/components/ui/use-id.d.ts.map +1 -0
  104. package/dist/v2/hooks/use-default-render-tool.d.ts +6 -0
  105. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  106. package/dist/v2/hooks/use-render-tool.d.ts +4 -0
  107. package/dist/v2/hooks/use-render-tool.d.ts.map +1 -1
  108. package/dist/v2/index.cjs +1 -1
  109. package/dist/v2/index.d.cts +3 -0
  110. package/dist/v2/index.d.mts +3 -0
  111. package/dist/v2/index.d.ts +3 -0
  112. package/dist/v2/index.d.ts.map +1 -1
  113. package/dist/v2/index.mjs +165 -162
  114. package/dist/v2/lib/message-feedback.d.ts +33 -0
  115. package/dist/v2/lib/message-feedback.d.ts.map +1 -0
  116. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  117. package/dist/v2/providers/CopilotKitProvider.types.d.ts +56 -1
  118. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  119. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +5 -1
  120. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  121. package/dist/v2/providers/index.d.ts +1 -1
  122. package/dist/v2/providers/index.d.ts.map +1 -1
  123. package/dist/v2/providers/keys.d.ts +8 -0
  124. package/dist/v2/providers/keys.d.ts.map +1 -1
  125. package/dist/v2/providers/types.d.ts +9 -0
  126. package/dist/v2/providers/types.d.ts.map +1 -1
  127. package/dist/v2/styles/CopilotKitStyles.d.ts +18 -0
  128. package/dist/v2/styles/CopilotKitStyles.d.ts.map +1 -0
  129. package/dist/v2/types/defineToolCallRenderer.d.ts +2 -0
  130. package/dist/v2/types/defineToolCallRenderer.d.ts.map +1 -1
  131. package/dist/v2/types/vue-tool-call-renderer.d.ts +7 -0
  132. package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
  133. package/package.json +11 -23
  134. package/src/components/copilot-provider/CopilotKit.vue +4 -1
  135. package/src/v2/__tests__/exports.test.ts +4 -2
  136. package/src/v2/components/A2UIMessageRenderer.ts +55 -67
  137. package/src/v2/components/A2UISurfaceActivityRenderer.vue +11 -23
  138. package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +8 -9
  139. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +63 -36
  140. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +14 -257
  141. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +444 -0
  142. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +175 -0
  143. package/src/v2/components/chat/BasicMarkdown.vue +8 -6
  144. package/src/v2/components/chat/CopilotChat.vue +7 -0
  145. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +267 -67
  146. package/src/v2/components/chat/CopilotChatAssistantToolbarButton.vue +46 -0
  147. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +141 -168
  148. package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +67 -18
  149. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +2 -2
  150. package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +240 -0
  151. package/src/v2/components/chat/CopilotChatInput.vue +210 -161
  152. package/src/v2/components/chat/CopilotChatInspectorButton.vue +331 -0
  153. package/src/v2/components/chat/CopilotChatLightbox.vue +105 -0
  154. package/src/v2/components/chat/CopilotChatMessageView.vue +261 -18
  155. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +21 -22
  156. package/src/v2/components/chat/CopilotChatSuggestionPill.vue +5 -14
  157. package/src/v2/components/chat/CopilotChatSuggestionView.vue +1 -4
  158. package/src/v2/components/chat/CopilotChatToggleButton.vue +3 -9
  159. package/src/v2/components/chat/CopilotChatToggleButtonCloseIcon.ts +1 -1
  160. package/src/v2/components/chat/CopilotChatToggleButtonOpenIcon.ts +1 -1
  161. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +330 -0
  162. package/src/v2/components/chat/CopilotChatToolCallItem.vue +21 -11
  163. package/src/v2/components/chat/CopilotChatToolCallsView.vue +192 -21
  164. package/src/v2/components/chat/CopilotChatUserMessage.vue +105 -42
  165. package/src/v2/components/chat/CopilotChatView.vue +33 -38
  166. package/src/v2/components/chat/CopilotChatWorkSummary.vue +85 -0
  167. package/src/v2/components/chat/CopilotModalHeader.vue +10 -8
  168. package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +7 -6
  169. package/src/v2/components/chat/CopilotModalHeaderTitle.ts +1 -4
  170. package/src/v2/components/chat/CopilotPopup.vue +3 -0
  171. package/src/v2/components/chat/CopilotPopupView.vue +1 -0
  172. package/src/v2/components/chat/CopilotPopupViewInternal.vue +9 -19
  173. package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +5 -8
  174. package/src/v2/components/chat/CopilotSidebar.vue +5 -0
  175. package/src/v2/components/chat/CopilotSidebarView.vue +2 -0
  176. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -17
  177. package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +7 -10
  178. package/src/v2/components/chat/StreamingMarkdownDefault.vue +3 -5
  179. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.inspector.test.ts +269 -0
  180. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.learningFeedback.test.ts +760 -0
  181. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.ts +3 -5
  182. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +2 -2
  183. package/src/v2/components/chat/__tests__/CopilotChatInput.addMenu.test.ts +328 -0
  184. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.ts +6 -3
  185. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +22 -13
  186. package/src/v2/components/chat/__tests__/CopilotChatLightbox.test.ts +160 -0
  187. package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +735 -0
  188. package/src/v2/components/chat/__tests__/CopilotChatSuggestionPill.test.ts +1 -1
  189. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.ts +3 -1
  190. package/src/v2/components/chat/__tests__/CopilotChatToolCallsView.learning.test.ts +75 -0
  191. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.attachments.test.ts +143 -0
  192. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.ts +5 -4
  193. package/src/v2/components/chat/__tests__/CopilotChatView.rootWidth.test.ts +7 -33
  194. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.ts +1 -1
  195. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +3 -3
  196. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +126 -0
  197. package/src/v2/components/chat/__tests__/messageFeedbackDefaults.test.ts +33 -0
  198. package/src/v2/components/chat/__tests__/modalChatDisplayProps.test.ts +44 -0
  199. package/src/v2/components/chat/index.ts +2 -0
  200. package/src/v2/components/chat/inspector-shortcuts.ts +34 -0
  201. package/src/v2/components/chat/message-feedback.ts +291 -0
  202. package/src/v2/components/chat/tool-call-capture.ts +55 -0
  203. package/src/v2/components/chat/tool-call-display-context.ts +49 -0
  204. package/src/v2/components/chat/tool-call-group-parts.ts +292 -0
  205. package/src/v2/components/chat/tool-call-layout.ts +304 -0
  206. package/src/v2/components/chat/turn-work.ts +61 -0
  207. package/src/v2/components/chat/types.ts +113 -3
  208. package/src/v2/components/chat/use-lightbox.ts +64 -0
  209. package/src/v2/components/icons/index.ts +4 -0
  210. package/src/v2/components/ui/CopilotDropdownMenuContent.vue +465 -0
  211. package/src/v2/components/ui/CopilotPopover.vue +138 -0
  212. package/src/v2/components/ui/CopilotTooltip.vue +237 -0
  213. package/src/v2/components/ui/__tests__/CopilotTooltip.test.ts +168 -0
  214. package/src/v2/components/ui/dropdown-menu.ts +15 -0
  215. package/src/v2/components/ui/floating.ts +321 -0
  216. package/src/v2/components/ui/presence.ts +54 -0
  217. package/src/v2/components/ui/use-id.ts +16 -0
  218. package/src/v2/hooks/use-default-render-tool.ts +123 -62
  219. package/src/v2/hooks/use-render-tool.ts +8 -0
  220. package/src/v2/index.ts +3 -0
  221. package/src/v2/lib/__tests__/message-feedback.test.ts +152 -0
  222. package/src/v2/lib/message-feedback.ts +120 -0
  223. package/src/v2/providers/CopilotChatConfigurationProvider.vue +29 -2
  224. package/src/v2/providers/CopilotKitProvider.types.ts +57 -0
  225. package/src/v2/providers/CopilotKitProvider.vue +125 -3
  226. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.openThread.test.ts +69 -0
  227. package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +534 -0
  228. package/src/v2/providers/index.ts +4 -1
  229. package/src/v2/providers/keys.ts +12 -0
  230. package/src/v2/providers/types.ts +11 -0
  231. package/src/v2/styles/CopilotKitStyles.ts +95 -0
  232. package/src/v2/styles/__tests__/CopilotKitStyles.client.test.ts +149 -0
  233. package/src/v2/styles/__tests__/CopilotKitStyles.ssr.test.ts +71 -0
  234. package/src/v2/types/defineToolCallRenderer.ts +4 -0
  235. package/src/v2/types/vue-tool-call-renderer.ts +7 -0
  236. package/vite.config.ts +2 -1
  237. package/dist/styles.css +0 -2
  238. package/scripts/scope-preflight.mjs +0 -100
  239. package/src/styles/globals.css +0 -233
@@ -649,7 +649,7 @@ describe("CopilotChatAssistantMessage Slot System E2E Tests", () => {
649
649
  });
650
650
 
651
651
  describe("5. className Override with Tailwind Strings", () => {
652
- it("should override root className while preserving default prose classes", () => {
652
+ it("should override root className while preserving default markdown classes", () => {
653
653
  const Host = defineComponent({
654
654
  components: { CopilotChatAssistantMessage },
655
655
  setup() {
@@ -664,10 +664,8 @@ describe("CopilotChatAssistantMessage Slot System E2E Tests", () => {
664
664
  const { container } = renderInWrapper(Host);
665
665
  const root = container.querySelector(".custom-root-class");
666
666
  expect(root).toBeDefined();
667
- const proseDiv =
668
- root?.classList.contains("cpk:prose") ||
669
- !!root?.querySelector(".cpk\\:prose");
670
- expect(proseDiv).toBe(true);
667
+ expect(root?.classList.contains("copilotKitAssistantMessage")).toBe(true);
668
+ expect(root?.querySelector(".cpk-markdown")).not.toBeNull();
671
669
  });
672
670
 
673
671
  it("should allow tailwind utilities to override default styles", () => {
@@ -907,7 +907,7 @@ describe("CopilotChatAssistantMessage", () => {
907
907
  <CopilotChatConfigurationProvider :thread-id="TEST_THREAD_ID">
908
908
  <CopilotChatAssistantMessage :message="basicMessage" :toolbar-visible="false">
909
909
  <template #layout="{ toolbar, toolbarVisible, message }">
910
- <span class="cpk:hidden">{{ childrenSpy(toolbar, toolbarVisible, message) }}</span>
910
+ <span hidden>{{ childrenSpy(toolbar, toolbarVisible, message) }}</span>
911
911
  <div data-testid="children-render" />
912
912
  </template>
913
913
  </CopilotChatAssistantMessage>
@@ -941,7 +941,7 @@ describe("CopilotChatAssistantMessage", () => {
941
941
  <CopilotChatConfigurationProvider :thread-id="TEST_THREAD_ID">
942
942
  <CopilotChatAssistantMessage :message="basicMessage">
943
943
  <template #layout="{ toolbar, toolbarVisible, message }">
944
- <span class="cpk:hidden">{{ childrenSpy(toolbar, toolbarVisible, message) }}</span>
944
+ <span hidden>{{ childrenSpy(toolbar, toolbarVisible, message) }}</span>
945
945
  <div />
946
946
  </template>
947
947
  </CopilotChatAssistantMessage>
@@ -0,0 +1,328 @@
1
+ import { defineComponent, h, nextTick } from "vue";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
5
+ import CopilotChatInput from "../CopilotChatInput.vue";
6
+ import type { ToolsMenuItem } from "../types";
7
+
8
+ const getAddButton = () =>
9
+ screen.getByTestId("copilot-chat-input-add") as HTMLButtonElement;
10
+
11
+ const pointer = (type: string, init: Record<string, unknown> = {}) =>
12
+ Object.assign(new Event(type, { bubbles: true, cancelable: true }), {
13
+ pointerType: "mouse",
14
+ button: 0,
15
+ ctrlKey: false,
16
+ ...init,
17
+ });
18
+
19
+ function renderInput(
20
+ props: Record<string, unknown> = {},
21
+ labels?: Record<string, string>,
22
+ ) {
23
+ return render(
24
+ defineComponent({
25
+ setup() {
26
+ return () =>
27
+ h(
28
+ CopilotChatConfigurationProvider,
29
+ { threadId: "test-thread", labels },
30
+ {
31
+ default: () =>
32
+ h(CopilotChatInput, {
33
+ autoFocus: false,
34
+ onSubmitMessage: () => {},
35
+ ...props,
36
+ }),
37
+ },
38
+ );
39
+ },
40
+ }),
41
+ );
42
+ }
43
+
44
+ const toolsMenu = (
45
+ spies: Record<string, () => void>,
46
+ ): (ToolsMenuItem | "-")[] => [
47
+ { label: "Insert date", action: spies.date! },
48
+ "-",
49
+ {
50
+ label: "Templates",
51
+ items: [
52
+ { label: "Bug report", action: spies.bug! },
53
+ { label: "Feature request", action: spies.feature! },
54
+ ],
55
+ },
56
+ { label: "Clear", action: spies.clear! },
57
+ ];
58
+
59
+ function spies() {
60
+ return {
61
+ date: vi.fn(),
62
+ bug: vi.fn(),
63
+ feature: vi.fn(),
64
+ clear: vi.fn(),
65
+ };
66
+ }
67
+
68
+ describe("CopilotChatInput add menu", () => {
69
+ afterEach(() => {
70
+ vi.useRealTimers();
71
+ document.body.innerHTML = "";
72
+ });
73
+
74
+ it("marks the trigger as a menu button and points it at the open menu", async () => {
75
+ renderInput({ toolsMenu: toolsMenu(spies()) });
76
+ const button = getAddButton();
77
+ expect(button.getAttribute("aria-haspopup")).toBe("menu");
78
+ expect(button.getAttribute("aria-expanded")).toBe("false");
79
+ expect(button.getAttribute("data-state")).toBe("closed");
80
+
81
+ await fireEvent.click(button);
82
+ const content = screen.getByRole("menu");
83
+ expect(button.getAttribute("aria-expanded")).toBe("true");
84
+ expect(button.getAttribute("data-state")).toBe("open");
85
+ expect(button.getAttribute("aria-controls")).toBe(content.id);
86
+ expect(content.getAttribute("aria-labelledby")).toBe(button.id);
87
+ expect(content.classList.contains("cpk-dropdown-menu-content")).toBe(true);
88
+ expect(content.getAttribute("data-slot")).toBe("dropdown-menu-content");
89
+ expect(content.hasAttribute("data-copilotkit")).toBe(true);
90
+ // Teleported out of the input, as Radix portals it.
91
+ expect(
92
+ screen.getByTestId("copilot-chat-input-container").contains(content),
93
+ ).toBe(false);
94
+ });
95
+
96
+ it("renders a submenu trigger instead of a flattened labelled group", async () => {
97
+ renderInput({ toolsMenu: toolsMenu(spies()) });
98
+ await fireEvent.click(getAddButton());
99
+
100
+ const subTrigger = screen.getByRole("menuitem", { name: "Templates" });
101
+ expect(subTrigger.getAttribute("aria-haspopup")).toBe("menu");
102
+ expect(subTrigger.getAttribute("aria-expanded")).toBe("false");
103
+ expect(subTrigger.classList.contains("cpk-dropdown-menu-sub-trigger")).toBe(
104
+ true,
105
+ );
106
+ expect(
107
+ subTrigger.querySelector(".cpk-dropdown-menu-sub-trigger-icon"),
108
+ ).not.toBeNull();
109
+ // The submenu's items are not in the menu until it opens.
110
+ expect(screen.queryByRole("menuitem", { name: "Bug report" })).toBeNull();
111
+ expect(document.querySelector(".cpk-dropdown-menu-label")).toBeNull();
112
+ expect(screen.getAllByRole("separator")).toHaveLength(1);
113
+ });
114
+
115
+ it("opens from the keyboard on its first item, and ArrowUp opens on the last", async () => {
116
+ renderInput({ toolsMenu: toolsMenu(spies()) });
117
+ const button = getAddButton();
118
+
119
+ await fireEvent.keyDown(button, { key: "ArrowDown" });
120
+ await waitFor(() =>
121
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
122
+ );
123
+
124
+ await fireEvent.keyDown(document.activeElement!, { key: "Escape" });
125
+ await waitFor(() => expect(document.activeElement).toBe(button));
126
+ expect(button.getAttribute("aria-expanded")).toBe("false");
127
+
128
+ await fireEvent.keyDown(button, { key: "ArrowUp" });
129
+ await waitFor(() =>
130
+ expect(document.activeElement?.textContent?.trim()).toBe("Clear"),
131
+ );
132
+ });
133
+
134
+ it("moves focus with the arrow keys, Home, End, and type-ahead", async () => {
135
+ renderInput({ toolsMenu: toolsMenu(spies()) });
136
+ await fireEvent.keyDown(getAddButton(), { key: "Enter" });
137
+ await waitFor(() =>
138
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
139
+ );
140
+ const focused = () => document.activeElement?.textContent?.trim();
141
+
142
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
143
+ expect(focused()).toBe("Templates");
144
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
145
+ expect(focused()).toBe("Clear");
146
+ // Radix menus do not wrap by default.
147
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
148
+ expect(focused()).toBe("Clear");
149
+ await fireEvent.keyDown(document.activeElement!, { key: "Home" });
150
+ expect(focused()).toBe("Insert date");
151
+ await fireEvent.keyDown(document.activeElement!, { key: "End" });
152
+ expect(focused()).toBe("Clear");
153
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowUp" });
154
+ expect(focused()).toBe("Templates");
155
+ await fireEvent.keyDown(document.activeElement!, { key: "i" });
156
+ expect(focused()).toBe("Insert date");
157
+ });
158
+
159
+ it("runs an item on Enter, closes, and returns focus to the trigger", async () => {
160
+ const actions = spies();
161
+ renderInput({ toolsMenu: toolsMenu(actions) });
162
+ const button = getAddButton();
163
+ await fireEvent.keyDown(button, { key: "Enter" });
164
+ await waitFor(() =>
165
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
166
+ );
167
+
168
+ await fireEvent.keyDown(document.activeElement!, { key: "Enter" });
169
+ expect(actions.date).toHaveBeenCalledTimes(1);
170
+ expect(button.getAttribute("aria-expanded")).toBe("false");
171
+ expect(document.activeElement).toBe(button);
172
+ });
173
+
174
+ it("opens a submenu with ArrowRight on its first item, and ArrowLeft closes it", async () => {
175
+ const actions = spies();
176
+ renderInput({ toolsMenu: toolsMenu(actions) });
177
+ await fireEvent.keyDown(getAddButton(), { key: "Enter" });
178
+ await waitFor(() =>
179
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
180
+ );
181
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
182
+ const subTrigger = screen.getByRole("menuitem", { name: "Templates" });
183
+ expect(document.activeElement).toBe(subTrigger);
184
+
185
+ await fireEvent.keyDown(subTrigger, { key: "ArrowRight" });
186
+ const bug = await screen.findByRole("menuitem", { name: "Bug report" });
187
+ await waitFor(() => expect(document.activeElement).toBe(bug));
188
+ expect(subTrigger.getAttribute("aria-expanded")).toBe("true");
189
+ const subContent = bug.closest('[role="menu"]') as HTMLElement;
190
+ expect(subContent.classList.contains("cpk-dropdown-menu-sub-content")).toBe(
191
+ true,
192
+ );
193
+ expect(subTrigger.getAttribute("aria-controls")).toBe(subContent.id);
194
+
195
+ await fireEvent.keyDown(bug, { key: "ArrowLeft" });
196
+ await waitFor(() => expect(document.activeElement).toBe(subTrigger));
197
+ expect(subTrigger.getAttribute("aria-expanded")).toBe("false");
198
+
199
+ // Enter on the submenu trigger opens it too; Enter on an item runs it.
200
+ await fireEvent.keyDown(subTrigger, { key: "Enter" });
201
+ const feature = await screen.findByRole("menuitem", {
202
+ name: "Feature request",
203
+ });
204
+ await waitFor(() =>
205
+ expect(document.activeElement?.textContent?.trim()).toBe("Bug report"),
206
+ );
207
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
208
+ expect(document.activeElement).toBe(feature);
209
+ await fireEvent.keyDown(feature, { key: "Enter" });
210
+ expect(actions.feature).toHaveBeenCalledTimes(1);
211
+ await waitFor(() => expect(screen.queryAllByRole("menu")).toHaveLength(0));
212
+ expect(document.activeElement).toBe(getAddButton());
213
+ });
214
+
215
+ it("closes the whole menu on Escape in a submenu without reaching outer Escape listeners", async () => {
216
+ renderInput({ toolsMenu: toolsMenu(spies()) });
217
+ const windowEscape = vi.fn();
218
+ const onWindowKeyDown = (event: KeyboardEvent) => {
219
+ if (event.key === "Escape") windowEscape();
220
+ };
221
+ window.addEventListener("keydown", onWindowKeyDown);
222
+ try {
223
+ await fireEvent.keyDown(getAddButton(), { key: "ArrowDown" });
224
+ await waitFor(() =>
225
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
226
+ );
227
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
228
+ await fireEvent.keyDown(document.activeElement!, { key: "ArrowRight" });
229
+ const bug = await screen.findByRole("menuitem", { name: "Bug report" });
230
+ await waitFor(() => expect(document.activeElement).toBe(bug));
231
+
232
+ await fireEvent.keyDown(bug, { key: "Escape" });
233
+ await waitFor(() =>
234
+ expect(getAddButton().getAttribute("aria-expanded")).toBe("false"),
235
+ );
236
+ expect(document.activeElement).toBe(getAddButton());
237
+ expect(windowEscape).not.toHaveBeenCalled();
238
+ } finally {
239
+ window.removeEventListener("keydown", onWindowKeyDown);
240
+ }
241
+ });
242
+
243
+ it("closes on Tab and returns focus to the trigger", async () => {
244
+ renderInput({ toolsMenu: toolsMenu(spies()) });
245
+ await fireEvent.keyDown(getAddButton(), { key: "ArrowDown" });
246
+ await waitFor(() =>
247
+ expect(document.activeElement?.textContent?.trim()).toBe("Insert date"),
248
+ );
249
+ await fireEvent.keyDown(document.activeElement!, { key: "Tab" });
250
+ expect(getAddButton().getAttribute("aria-expanded")).toBe("false");
251
+ expect(document.activeElement).toBe(getAddButton());
252
+ });
253
+
254
+ it("opens on pointer down, and a pointer down outside closes it", async () => {
255
+ renderInput({ toolsMenu: toolsMenu(spies()) });
256
+ const button = getAddButton();
257
+ button.dispatchEvent(pointer("pointerdown"));
258
+ // The click that follows the pointer down does not toggle it again.
259
+ await fireEvent.click(button, { detail: 1 });
260
+ await nextTick();
261
+ expect(button.getAttribute("aria-expanded")).toBe("true");
262
+ const content = screen.getByRole("menu");
263
+ await waitFor(() => expect(document.activeElement).toBe(content));
264
+
265
+ document.body.dispatchEvent(pointer("pointerdown"));
266
+ await nextTick();
267
+ expect(button.getAttribute("aria-expanded")).toBe("false");
268
+ });
269
+
270
+ it("opens a submenu on hover and focuses the hovered item", async () => {
271
+ vi.useFakeTimers();
272
+ const actions = spies();
273
+ renderInput({ toolsMenu: toolsMenu(actions) });
274
+ await fireEvent.click(getAddButton(), { detail: 1 });
275
+ await nextTick();
276
+ const subTrigger = screen.getByRole("menuitem", { name: "Templates" });
277
+
278
+ subTrigger.dispatchEvent(pointer("pointermove"));
279
+ expect(document.activeElement).toBe(subTrigger);
280
+ expect(screen.queryByRole("menuitem", { name: "Bug report" })).toBeNull();
281
+ await vi.advanceTimersByTimeAsync(150);
282
+ const bug = screen.getByRole("menuitem", { name: "Bug report" });
283
+ // Opening on hover leaves focus on the trigger.
284
+ expect(document.activeElement).toBe(subTrigger);
285
+
286
+ await fireEvent.click(bug);
287
+ expect(actions.bug).toHaveBeenCalledTimes(1);
288
+ await vi.advanceTimersByTimeAsync(500);
289
+ expect(screen.queryAllByRole("menu")).toHaveLength(0);
290
+ });
291
+
292
+ it("shows the add button label and the / shortcut in its tooltip", async () => {
293
+ renderInput(
294
+ { onAddFile: vi.fn() },
295
+ { chatInputToolbarAddButtonLabel: "Upload attachment" },
296
+ );
297
+ const button = getAddButton();
298
+ await fireEvent.focus(button);
299
+ const tooltip = screen.getByRole("tooltip");
300
+ expect(tooltip.textContent).toContain("Upload attachment");
301
+ expect(tooltip.querySelector("code")?.textContent?.trim()).toBe("/");
302
+ expect(
303
+ tooltip.querySelector(".cpk-chat-input-add-menu-tooltip"),
304
+ ).not.toBeNull();
305
+ expect(button.getAttribute("aria-describedby")).toBe(tooltip.id);
306
+ });
307
+
308
+ it("hides the add button tooltip while the menu is open", async () => {
309
+ renderInput({ onAddFile: vi.fn() });
310
+ const button = getAddButton();
311
+ await fireEvent.focus(button);
312
+ expect(screen.getByRole("tooltip")).toBeTruthy();
313
+
314
+ await fireEvent.keyDown(button, { key: "Enter" });
315
+ await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull());
316
+ });
317
+
318
+ it("gives the transcribe buttons a tooltip with their label instead of a title", async () => {
319
+ renderInput({ onStartTranscribe: vi.fn() });
320
+ const start = screen.getByTestId("copilot-chat-input-start-transcribe");
321
+ expect(start.hasAttribute("title")).toBe(false);
322
+ await fireEvent.focus(start);
323
+ const tooltip = screen.getByRole("tooltip");
324
+ expect(tooltip.textContent?.trim()).toBe("Transcribe");
325
+ expect(tooltip.getAttribute("data-side")).toBe("bottom");
326
+ expect(start.getAttribute("aria-describedby")).toBe(tooltip.id);
327
+ });
328
+ });
@@ -992,7 +992,9 @@ describe("CopilotChatInput Slot System E2E Tests", () => {
992
992
  "[data-testid='input-container']",
993
993
  ) as HTMLElement;
994
994
  expect(inputContainer).not.toBeNull();
995
- expect(inputContainer.classList.contains("cpk:absolute")).toBe(false);
995
+ expect(inputContainer.classList.contains("cpk-chat-input-absolute")).toBe(
996
+ false,
997
+ );
996
998
  });
997
999
 
998
1000
  it("should render absolute positioning when positioning='absolute'", () => {
@@ -1008,8 +1010,9 @@ describe("CopilotChatInput Slot System E2E Tests", () => {
1008
1010
  "[data-testid='absolute-input']",
1009
1011
  ) as HTMLElement;
1010
1012
  expect(inputContainer).not.toBeNull();
1011
- expect(inputContainer.classList.contains("cpk:absolute")).toBe(true);
1012
- expect(inputContainer.classList.contains("cpk:bottom-0")).toBe(true);
1013
+ expect(inputContainer.classList.contains("cpk-chat-input-absolute")).toBe(
1014
+ true,
1015
+ );
1013
1016
  });
1014
1017
 
1015
1018
  it("should apply keyboard height transform", () => {
@@ -35,7 +35,9 @@ const mockLayoutMetrics = (
35
35
  container: HTMLElement,
36
36
  options?: { gridWidth?: number; addWidth?: number; actionsWidth?: number },
37
37
  ) => {
38
- const grid = container.querySelector("div.cpk\\:grid") as HTMLElement | null;
38
+ const grid = container.querySelector(
39
+ ".cpk-chat-input-grid",
40
+ ) as HTMLElement | null;
39
41
  if (!grid) {
40
42
  throw new Error("Grid container not found in CopilotChatInput layout");
41
43
  }
@@ -694,9 +696,8 @@ describe("CopilotChatInput", () => {
694
696
  const layoutCell = textarea.parentElement as HTMLElement;
695
697
  const gridContainer = layoutCell.parentElement as HTMLElement;
696
698
 
697
- expect(layoutCell.className).toContain("col-start-2");
698
- expect(layoutCell.className).not.toContain("col-span-3");
699
- expect(gridContainer.className).toContain("items-center");
699
+ expect(layoutCell.className).toContain("cpk-chat-input-field");
700
+ expect(gridContainer.getAttribute("data-layout")).toBe("compact");
700
701
  });
701
702
 
702
703
  it("toggles textarea padding based on multiline state", async () => {
@@ -706,8 +707,10 @@ describe("CopilotChatInput", () => {
706
707
  mockLayoutMetrics(container);
707
708
 
708
709
  const textarea = screen.getByRole("textbox");
709
- expect(textarea.className).toContain("pr-5");
710
- expect(textarea.className).not.toContain("px-5");
710
+ expect(textarea.className).toContain("cpk-chat-input-textarea-compact");
711
+ expect(textarea.className).not.toContain(
712
+ "cpk-chat-input-textarea-expanded",
713
+ );
711
714
 
712
715
  await fireEvent.input(textarea, {
713
716
  target: {
@@ -717,8 +720,10 @@ describe("CopilotChatInput", () => {
717
720
  });
718
721
 
719
722
  await waitFor(() => {
720
- expect(textarea.className).toContain("px-5");
721
- expect(textarea.className).not.toContain("pr-5");
723
+ expect(textarea.className).toContain("cpk-chat-input-textarea-expanded");
724
+ expect(textarea.className).not.toContain(
725
+ "cpk-chat-input-textarea-compact",
726
+ );
722
727
  });
723
728
  });
724
729
 
@@ -738,13 +743,16 @@ describe("CopilotChatInput", () => {
738
743
  },
739
744
  });
740
745
  await waitFor(() => {
741
- expect(layoutCell.className).toContain("col-span-3");
746
+ expect(layoutCell.parentElement?.getAttribute("data-layout")).toBe(
747
+ "expanded",
748
+ );
742
749
  });
743
750
 
744
751
  await fireEvent.input(textarea, { target: { value: "short" } });
745
752
  await waitFor(() => {
746
- expect(layoutCell.className).toContain("col-start-2");
747
- expect(layoutCell.className).not.toContain("col-span-3");
753
+ expect(layoutCell.parentElement?.getAttribute("data-layout")).toBe(
754
+ "compact",
755
+ );
748
756
  });
749
757
  });
750
758
 
@@ -760,8 +768,9 @@ describe("CopilotChatInput", () => {
760
768
 
761
769
  await waitFor(() => {
762
770
  const layoutCell = textarea.parentElement as HTMLElement;
763
- expect(layoutCell.className).toContain("col-span-3");
764
- expect(layoutCell.className).not.toContain("col-start-2");
771
+ expect(layoutCell.parentElement?.getAttribute("data-layout")).toBe(
772
+ "expanded",
773
+ );
765
774
  });
766
775
  });
767
776
 
@@ -0,0 +1,160 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { nextTick } from "vue";
3
+ import { fireEvent, render, screen } from "@testing-library/vue";
4
+ import CopilotChatAttachmentRenderer from "../CopilotChatAttachmentRenderer.vue";
5
+
6
+ // Ported from react-core's lightbox and attachment renderer behavior.
7
+
8
+ const image = {
9
+ type: "image" as const,
10
+ source: { type: "url" as const, value: "https://example.com/cat.png" },
11
+ };
12
+
13
+ async function openLightbox() {
14
+ const result = render(CopilotChatAttachmentRenderer, { props: image });
15
+ const thumbnail = screen.getByRole("button", { name: "Image attachment" });
16
+ thumbnail.focus();
17
+ await fireEvent.click(thumbnail);
18
+ await nextTick();
19
+ return { ...result, thumbnail };
20
+ }
21
+
22
+ describe("CopilotChatAttachmentRenderer lightbox", () => {
23
+ afterEach(() => {
24
+ document.body.innerHTML = "";
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ it("opens the image full size in a lightbox portalled to the body", async () => {
29
+ const { container } = await openLightbox();
30
+
31
+ const dialog = screen.getByRole("dialog", { name: "Image attachment" });
32
+ expect(dialog.classList.contains("cpk-attachment-lightbox")).toBe(true);
33
+ expect(container.contains(dialog)).toBe(false);
34
+ const full = dialog.querySelector("img");
35
+ expect(full?.getAttribute("src")).toBe("https://example.com/cat.png");
36
+ expect(
37
+ full?.classList.contains("cpk-attachment-renderer-lightbox-image"),
38
+ ).toBe(true);
39
+ });
40
+
41
+ it("names the close button and moves focus to it", async () => {
42
+ await openLightbox();
43
+ const close = screen.getByRole("button", { name: "Close preview" });
44
+ expect(
45
+ close.classList.contains("cpk-attachment-lightbox-close-button"),
46
+ ).toBe(true);
47
+ expect(document.activeElement).toBe(close);
48
+ });
49
+
50
+ it("closes on Escape and returns focus to the thumbnail", async () => {
51
+ const { thumbnail } = await openLightbox();
52
+ await fireEvent.keyDown(document, { key: "Escape" });
53
+ await nextTick();
54
+ expect(screen.queryByRole("dialog")).toBeNull();
55
+ expect(document.activeElement).toBe(thumbnail);
56
+ });
57
+
58
+ it("closes on the close button and on a backdrop click, not a content click", async () => {
59
+ await openLightbox();
60
+ await fireEvent.click(screen.getByRole("dialog").querySelector("img")!);
61
+ expect(screen.queryByRole("dialog")).not.toBeNull();
62
+
63
+ await fireEvent.click(screen.getByRole("dialog"));
64
+ expect(screen.queryByRole("dialog")).toBeNull();
65
+
66
+ await fireEvent.click(
67
+ screen.getByRole("button", { name: "Image attachment" }),
68
+ );
69
+ await fireEvent.click(
70
+ screen.getByRole("button", { name: "Close preview" }),
71
+ );
72
+ expect(screen.queryByRole("dialog")).toBeNull();
73
+ });
74
+
75
+ it("keeps Tab inside the lightbox", async () => {
76
+ await openLightbox();
77
+ const close = screen.getByRole("button", { name: "Close preview" });
78
+ const event = new KeyboardEvent("keydown", {
79
+ key: "Tab",
80
+ bubbles: true,
81
+ cancelable: true,
82
+ });
83
+ close.dispatchEvent(event);
84
+ expect(event.defaultPrevented).toBe(true);
85
+ expect(document.activeElement).toBe(close);
86
+ });
87
+
88
+ it("morphs the thumbnail with a view transition when the browser has one", async () => {
89
+ const names: string[] = [];
90
+ const startViewTransition = vi.fn((callback: () => Promise<void>) => {
91
+ const thumbnail = document.querySelector<HTMLElement>(
92
+ ".cpk-attachment-renderer-image",
93
+ )!;
94
+ names.push(thumbnail.style.viewTransitionName);
95
+ const done = callback();
96
+ return { finished: done.then(() => undefined) };
97
+ });
98
+ Object.defineProperty(document, "startViewTransition", {
99
+ value: startViewTransition,
100
+ configurable: true,
101
+ });
102
+ try {
103
+ await openLightbox();
104
+ await nextTick();
105
+ expect(startViewTransition).toHaveBeenCalledTimes(1);
106
+ // The name is on the thumbnail before the transition, then on the
107
+ // lightbox image.
108
+ expect(names[0]).toMatch(/^cpk-lightbox-/);
109
+ const full = screen.getByRole("dialog").querySelector("img")!;
110
+ expect(full.style.viewTransitionName).toBe(names[0]);
111
+ expect(
112
+ document.querySelector<HTMLElement>(".cpk-attachment-renderer-image")!
113
+ .style.viewTransitionName,
114
+ ).toBe("");
115
+ } finally {
116
+ delete (document as { startViewTransition?: unknown })
117
+ .startViewTransition;
118
+ }
119
+ });
120
+ });
121
+
122
+ describe("CopilotChatAttachmentRenderer document link", () => {
123
+ it("links a document attachment to its resolved source URL", () => {
124
+ const href =
125
+ "https://files.example.test/download/document.pdf?signature=preserved";
126
+ render(CopilotChatAttachmentRenderer, {
127
+ props: {
128
+ type: "document",
129
+ source: { type: "url", value: href, mimeType: "application/pdf" },
130
+ filename: "document.pdf",
131
+ },
132
+ });
133
+
134
+ const link = screen.getByRole("link", { name: "document.pdf" });
135
+ expect(link.getAttribute("href")).toBe(href);
136
+ expect(link.getAttribute("download")).toBe("document.pdf");
137
+ expect(link.getAttribute("target")).toBe("_blank");
138
+ expect(link.getAttribute("rel")).toBe("noopener noreferrer");
139
+ expect(link.textContent).toContain("document.pdf");
140
+ });
141
+
142
+ it("keeps a data URL downloadable when no filename is known", () => {
143
+ render(CopilotChatAttachmentRenderer, {
144
+ props: {
145
+ type: "document",
146
+ source: {
147
+ type: "data",
148
+ value: "JVBERi0=",
149
+ mimeType: "application/pdf",
150
+ },
151
+ },
152
+ });
153
+
154
+ const link = screen.getByRole("link", { name: "application/pdf" });
155
+ expect(link.getAttribute("href")).toBe(
156
+ "data:application/pdf;base64,JVBERi0=",
157
+ );
158
+ expect(link.getAttribute("download")).toBe("");
159
+ });
160
+ });