@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.
- package/PARITY.md +13 -12
- package/README.md +1 -2
- package/dist/components/copilot-provider/CopilotKit.vue.d.ts +3 -1
- package/dist/components/copilot-provider/CopilotKit.vue.d.ts.map +1 -1
- package/dist/index.cjs +43 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +10170 -7506
- package/dist/index.mjs.map +1 -1
- package/dist/v2/components/A2UIMessageRenderer.d.ts +7 -0
- package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
- package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +11 -2
- package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
- package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +136 -0
- package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -0
- package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChat.vue.d.ts +1 -0
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +3 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts +16 -0
- package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts +22 -0
- package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +69 -16
- package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts +12 -0
- package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts +22 -0
- package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +20 -0
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatSuggestionPill.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatSuggestionView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToggleButton.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +32 -0
- package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +11 -1
- package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -0
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts +5 -0
- package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotModalHeaderTitle.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopup.vue.d.ts +1 -0
- package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopupView.vue.d.ts +1 -0
- package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts +1 -0
- package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebar.vue.d.ts +2 -0
- package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +2 -0
- package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +2 -0
- package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/index.d.ts +17 -0
- package/dist/v2/components/chat/index.d.ts.map +1 -1
- package/dist/v2/components/chat/inspector-shortcuts.d.ts +12 -0
- package/dist/v2/components/chat/inspector-shortcuts.d.ts.map +1 -0
- package/dist/v2/components/chat/message-feedback.d.ts +89 -0
- package/dist/v2/components/chat/message-feedback.d.ts.map +1 -0
- package/dist/v2/components/chat/tool-call-capture.d.ts +39 -0
- package/dist/v2/components/chat/tool-call-capture.d.ts.map +1 -0
- package/dist/v2/components/chat/tool-call-display-context.d.ts +39 -0
- package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -0
- package/dist/v2/components/chat/tool-call-group-parts.d.ts +144 -0
- package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -0
- package/dist/v2/components/chat/tool-call-layout.d.ts +115 -0
- package/dist/v2/components/chat/tool-call-layout.d.ts.map +1 -0
- package/dist/v2/components/chat/turn-work.d.ts +35 -0
- package/dist/v2/components/chat/turn-work.d.ts.map +1 -0
- package/dist/v2/components/chat/types.d.ts +104 -4
- package/dist/v2/components/chat/types.d.ts.map +1 -1
- package/dist/v2/components/chat/use-lightbox.d.ts +18 -0
- package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -0
- package/dist/v2/components/icons/index.d.ts +1 -1
- package/dist/v2/components/icons/index.d.ts.map +1 -1
- package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +55 -0
- package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -0
- package/dist/v2/components/ui/CopilotPopover.vue.d.ts +40 -0
- package/dist/v2/components/ui/CopilotPopover.vue.d.ts.map +1 -0
- package/dist/v2/components/ui/CopilotTooltip.vue.d.ts +37 -0
- package/dist/v2/components/ui/CopilotTooltip.vue.d.ts.map +1 -0
- package/dist/v2/components/ui/dropdown-menu.d.ts +14 -0
- package/dist/v2/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/v2/components/ui/floating.d.ts +84 -0
- package/dist/v2/components/ui/floating.d.ts.map +1 -0
- package/dist/v2/components/ui/presence.d.ts +9 -0
- package/dist/v2/components/ui/presence.d.ts.map +1 -0
- package/dist/v2/components/ui/use-id.d.ts +9 -0
- package/dist/v2/components/ui/use-id.d.ts.map +1 -0
- package/dist/v2/hooks/use-default-render-tool.d.ts +6 -0
- package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
- package/dist/v2/hooks/use-render-tool.d.ts +4 -0
- package/dist/v2/hooks/use-render-tool.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.d.cts +3 -0
- package/dist/v2/index.d.mts +3 -0
- package/dist/v2/index.d.ts +3 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.mjs +165 -162
- package/dist/v2/lib/message-feedback.d.ts +33 -0
- package/dist/v2/lib/message-feedback.d.ts.map +1 -0
- package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.types.d.ts +56 -1
- package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts +5 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/index.d.ts +1 -1
- package/dist/v2/providers/index.d.ts.map +1 -1
- package/dist/v2/providers/keys.d.ts +8 -0
- package/dist/v2/providers/keys.d.ts.map +1 -1
- package/dist/v2/providers/types.d.ts +9 -0
- package/dist/v2/providers/types.d.ts.map +1 -1
- package/dist/v2/styles/CopilotKitStyles.d.ts +18 -0
- package/dist/v2/styles/CopilotKitStyles.d.ts.map +1 -0
- package/dist/v2/types/defineToolCallRenderer.d.ts +2 -0
- package/dist/v2/types/defineToolCallRenderer.d.ts.map +1 -1
- package/dist/v2/types/vue-tool-call-renderer.d.ts +7 -0
- package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
- package/package.json +11 -23
- package/src/components/copilot-provider/CopilotKit.vue +4 -1
- package/src/v2/__tests__/exports.test.ts +4 -2
- package/src/v2/components/A2UIMessageRenderer.ts +55 -67
- package/src/v2/components/A2UISurfaceActivityRenderer.vue +11 -23
- package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +8 -9
- package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +63 -36
- package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +14 -257
- package/src/v2/components/a2ui/A2UIRecoveryStates.ts +444 -0
- package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +175 -0
- package/src/v2/components/chat/BasicMarkdown.vue +8 -6
- package/src/v2/components/chat/CopilotChat.vue +7 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.vue +267 -67
- package/src/v2/components/chat/CopilotChatAssistantToolbarButton.vue +46 -0
- package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +141 -168
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +67 -18
- package/src/v2/components/chat/CopilotChatAudioRecorder.vue +2 -2
- package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +240 -0
- package/src/v2/components/chat/CopilotChatInput.vue +210 -161
- package/src/v2/components/chat/CopilotChatInspectorButton.vue +331 -0
- package/src/v2/components/chat/CopilotChatLightbox.vue +105 -0
- package/src/v2/components/chat/CopilotChatMessageView.vue +261 -18
- package/src/v2/components/chat/CopilotChatReasoningMessage.vue +21 -22
- package/src/v2/components/chat/CopilotChatSuggestionPill.vue +5 -14
- package/src/v2/components/chat/CopilotChatSuggestionView.vue +1 -4
- package/src/v2/components/chat/CopilotChatToggleButton.vue +3 -9
- package/src/v2/components/chat/CopilotChatToggleButtonCloseIcon.ts +1 -1
- package/src/v2/components/chat/CopilotChatToggleButtonOpenIcon.ts +1 -1
- package/src/v2/components/chat/CopilotChatToolCallGroup.vue +330 -0
- package/src/v2/components/chat/CopilotChatToolCallItem.vue +21 -11
- package/src/v2/components/chat/CopilotChatToolCallsView.vue +192 -21
- package/src/v2/components/chat/CopilotChatUserMessage.vue +105 -42
- package/src/v2/components/chat/CopilotChatView.vue +33 -38
- package/src/v2/components/chat/CopilotChatWorkSummary.vue +85 -0
- package/src/v2/components/chat/CopilotModalHeader.vue +10 -8
- package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +7 -6
- package/src/v2/components/chat/CopilotModalHeaderTitle.ts +1 -4
- package/src/v2/components/chat/CopilotPopup.vue +3 -0
- package/src/v2/components/chat/CopilotPopupView.vue +1 -0
- package/src/v2/components/chat/CopilotPopupViewInternal.vue +9 -19
- package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +5 -8
- package/src/v2/components/chat/CopilotSidebar.vue +5 -0
- package/src/v2/components/chat/CopilotSidebarView.vue +2 -0
- package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -17
- package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +7 -10
- package/src/v2/components/chat/StreamingMarkdownDefault.vue +3 -5
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.inspector.test.ts +269 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.learningFeedback.test.ts +760 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.ts +3 -5
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +2 -2
- package/src/v2/components/chat/__tests__/CopilotChatInput.addMenu.test.ts +328 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.ts +6 -3
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +22 -13
- package/src/v2/components/chat/__tests__/CopilotChatLightbox.test.ts +160 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +735 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionPill.test.ts +1 -1
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.ts +3 -1
- package/src/v2/components/chat/__tests__/CopilotChatToolCallsView.learning.test.ts +75 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.attachments.test.ts +143 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.ts +5 -4
- package/src/v2/components/chat/__tests__/CopilotChatView.rootWidth.test.ts +7 -33
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.ts +1 -1
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +3 -3
- package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +126 -0
- package/src/v2/components/chat/__tests__/messageFeedbackDefaults.test.ts +33 -0
- package/src/v2/components/chat/__tests__/modalChatDisplayProps.test.ts +44 -0
- package/src/v2/components/chat/index.ts +2 -0
- package/src/v2/components/chat/inspector-shortcuts.ts +34 -0
- package/src/v2/components/chat/message-feedback.ts +291 -0
- package/src/v2/components/chat/tool-call-capture.ts +55 -0
- package/src/v2/components/chat/tool-call-display-context.ts +49 -0
- package/src/v2/components/chat/tool-call-group-parts.ts +292 -0
- package/src/v2/components/chat/tool-call-layout.ts +304 -0
- package/src/v2/components/chat/turn-work.ts +61 -0
- package/src/v2/components/chat/types.ts +113 -3
- package/src/v2/components/chat/use-lightbox.ts +64 -0
- package/src/v2/components/icons/index.ts +4 -0
- package/src/v2/components/ui/CopilotDropdownMenuContent.vue +465 -0
- package/src/v2/components/ui/CopilotPopover.vue +138 -0
- package/src/v2/components/ui/CopilotTooltip.vue +237 -0
- package/src/v2/components/ui/__tests__/CopilotTooltip.test.ts +168 -0
- package/src/v2/components/ui/dropdown-menu.ts +15 -0
- package/src/v2/components/ui/floating.ts +321 -0
- package/src/v2/components/ui/presence.ts +54 -0
- package/src/v2/components/ui/use-id.ts +16 -0
- package/src/v2/hooks/use-default-render-tool.ts +123 -62
- package/src/v2/hooks/use-render-tool.ts +8 -0
- package/src/v2/index.ts +3 -0
- package/src/v2/lib/__tests__/message-feedback.test.ts +152 -0
- package/src/v2/lib/message-feedback.ts +120 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.vue +29 -2
- package/src/v2/providers/CopilotKitProvider.types.ts +57 -0
- package/src/v2/providers/CopilotKitProvider.vue +125 -3
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.openThread.test.ts +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +534 -0
- package/src/v2/providers/index.ts +4 -1
- package/src/v2/providers/keys.ts +12 -0
- package/src/v2/providers/types.ts +11 -0
- package/src/v2/styles/CopilotKitStyles.ts +95 -0
- package/src/v2/styles/__tests__/CopilotKitStyles.client.test.ts +149 -0
- package/src/v2/styles/__tests__/CopilotKitStyles.ssr.test.ts +71 -0
- package/src/v2/types/defineToolCallRenderer.ts +4 -0
- package/src/v2/types/vue-tool-call-renderer.ts +7 -0
- package/vite.config.ts +2 -1
- package/dist/styles.css +0 -2
- package/scripts/scope-preflight.mjs +0 -100
- 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
|
|
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
|
-
|
|
668
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1012
|
-
|
|
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(
|
|
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("
|
|
698
|
-
expect(
|
|
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("
|
|
710
|
-
expect(textarea.className).not.toContain(
|
|
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("
|
|
721
|
-
expect(textarea.className).not.toContain(
|
|
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.
|
|
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.
|
|
747
|
-
|
|
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.
|
|
764
|
-
|
|
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
|
+
});
|