66} from "@t3tools/contracts" ;
77import { useState , type ReactNode } from "react" ;
88import { describe , expect , it , vi } from "vite-plus/test" ;
9- import { userEvent } from "vite-plus/test/browser" ;
9+ import { page , userEvent } from "vite-plus/test/browser" ;
1010
1111import type { AssistantCitationSourceAnchor } from "~/lib/assistantTextSelection" ;
1212import "~/index.css" ;
@@ -20,7 +20,7 @@ vi.mock(import("./AssistantCitationSource"), async (importOriginal) => ({
2020vi . mock ( import ( "@tanstack/react-router" ) , async ( importOriginal ) => ( {
2121 ...( await importOriginal ( ) ) ,
2222 useNavigate : ( ) => vi . fn ( ) ,
23- Link : ( ( { children } : { children : ReactNode } ) => < a > { children } </ a > ) as never ,
23+ Link : ( ( { children } : { children : ReactNode } ) => < a href = "#source" > { children } </ a > ) as never ,
2424} ) ) ;
2525
2626import { AssistantCitationChip } from "./AssistantCitationChip" ;
@@ -50,14 +50,23 @@ function setup() {
5050 return { sourceAnchor, [ Symbol . dispose ] : ( ) => source . remove ( ) } ;
5151}
5252
53- function mount ( sourceAnchor : AssistantCitationSourceAnchor , onSave : ( ) => boolean ) {
53+ function mount (
54+ sourceAnchor : AssistantCitationSourceAnchor ,
55+ onSave : ( ) => boolean ,
56+ onCancel ?: ( ) => void ,
57+ ) {
5458 function Composer ( ) {
5559 const [ open , setOpen ] = useState ( true ) ;
5660 return (
5761 < AssistantCitationChip
5862 citation = { citation }
59- composer
60- commentEditor = { { open, sourceAnchor, onOpenChange : setOpen , onSave } }
63+ commentEditor = { {
64+ open,
65+ sourceAnchor,
66+ onOpenChange : setOpen ,
67+ onSave,
68+ ...( onCancel ? { onCancel } : { } ) ,
69+ } }
6170 />
6271 ) ;
6372 }
@@ -83,12 +92,10 @@ async function removeSource() {
8392 await act ( async ( ) => onUnavailable ( ) ) ;
8493}
8594
86- async function clickButton ( label : string ) {
87- const button = [ ...document . querySelectorAll ( "button" ) ] . find (
88- ( element ) => element . textContent ?. trim ( ) === label ,
89- ) ;
90- expect ( button , `button ${ label } ` ) . toBeDefined ( ) ;
91- await act ( async ( ) => button ! . click ( ) ) ;
95+ // Once the source is gone the popup moves to the chip, which saves with Enter.
96+ async function pressEnter ( ) {
97+ textarea ( ) ! . focus ( ) ;
98+ await userEvent . keyboard ( "{Enter}" ) ;
9299}
93100
94101describe ( "citation comment source disappearance" , ( ) => {
@@ -107,7 +114,7 @@ describe("citation comment source disappearance", () => {
107114 expect ( mocks . observeSource ) . toHaveBeenCalledTimes ( 1 ) ;
108115
109116 await typeComment ( "shortened comment" ) ;
110- await clickButton ( "Save" ) ;
117+ await pressEnter ( ) ;
111118 expect ( onSave ) . toHaveBeenCalledWith ( "shortened comment" ) ;
112119 expect ( textarea ( ) ) . toBeNull ( ) ;
113120 } ) ;
@@ -122,10 +129,26 @@ describe("citation comment source disappearance", () => {
122129 expect ( onSave ) . toHaveBeenCalledWith ( "keep this draft" ) ;
123130 expect ( textarea ( ) ?. value ) . toBe ( "keep this draft" ) ;
124131 onSave . mockReturnValue ( true ) ;
125- await clickButton ( "Save" ) ;
132+ await pressEnter ( ) ;
126133 expect ( onSave ) . toHaveBeenLastCalledWith ( "keep this draft" ) ;
127134 expect ( textarea ( ) ) . toBeNull ( ) ;
128135 } ) ;
136+
137+ it ( "removes a just-inserted quote on Escape from Go to source" , async ( ) => {
138+ using source = setup ( ) ;
139+ const onCancel = vi . fn ( ) ;
140+ await using _screen = await mount (
141+ source . sourceAnchor ,
142+ vi . fn ( ( ) => true ) ,
143+ onCancel ,
144+ ) ;
145+ await typeComment ( "x" . repeat ( ASSISTANT_CITATION_MAX_COMMENT_LENGTH + 1 ) ) ;
146+ await removeSource ( ) ;
147+
148+ page . getByRole ( "link" , { name : "Go to source" } ) . element ( ) . focus ( ) ;
149+ await userEvent . keyboard ( "{Escape}" ) ;
150+ expect ( onCancel ) . toHaveBeenCalledTimes ( 1 ) ;
151+ } ) ;
129152} ) ;
130153
131154describe ( "citation comment popup position" , ( ) => {
@@ -175,3 +198,57 @@ describe("citation comment popup position", () => {
175198 expect ( rect . right ) . toBeLessThanOrEqual ( window . innerWidth ) ;
176199 } ) ;
177200} ) ;
201+
202+ describe ( "draft citation chip" , ( ) => {
203+ it ( "opens the quote with an editable comment, ready to type at its end" , async ( ) => {
204+ using _source = setup ( ) ;
205+ const onSave = vi . fn ( ( ) => true ) ;
206+ function Composer ( ) {
207+ const [ open , setOpen ] = useState ( false ) ;
208+ return (
209+ < AssistantCitationChip
210+ citation = { { ...citation , comment : "note" } }
211+ commentEditor = { { open, onOpenChange : setOpen , onSave } }
212+ />
213+ ) ;
214+ }
215+ await using _screen = await render ( < Composer /> ) ;
216+
217+ await page . getByRole ( "button" , { name : "Edit citation comment" } ) . click ( ) ;
218+
219+ await expect . element ( page . getByRole ( "blockquote" ) ) . toHaveTextContent ( "hello" ) ;
220+ await expect . element ( page . getByText ( "Go to source" ) ) . toBeVisible ( ) ;
221+ await vi . waitFor ( ( ) => expect ( document . activeElement ) . toBe ( textarea ( ) ) ) ;
222+ expect ( textarea ( ) ?. selectionStart ) . toBe ( "note" . length ) ;
223+ expect ( page . getByRole ( "button" , { name : "Save" } ) . query ( ) ) . toBeNull ( ) ;
224+
225+ await userEvent . keyboard ( " again{Enter}" ) ;
226+ expect ( onSave ) . toHaveBeenCalledWith ( "note again" ) ;
227+ expect ( textarea ( ) ) . toBeNull ( ) ;
228+ } ) ;
229+
230+ it ( "keeps the comment field and its caret in view under a long quote and comment" , async ( ) => {
231+ using _source = setup ( ) ;
232+ function Composer ( ) {
233+ const [ open , setOpen ] = useState ( false ) ;
234+ return (
235+ < AssistantCitationChip
236+ citation = { {
237+ ...citation ,
238+ text : "long line\n" . repeat ( 80 ) ,
239+ comment : "long comment\n" . repeat ( 40 ) ,
240+ } }
241+ commentEditor = { { open, onOpenChange : setOpen , onSave : ( ) => true } }
242+ />
243+ ) ;
244+ }
245+ await using _screen = await render ( < Composer /> ) ;
246+
247+ await page . getByRole ( "button" , { name : "Edit citation comment" } ) . click ( ) ;
248+
249+ await vi . waitFor ( ( ) => expect ( document . activeElement ) . toBe ( textarea ( ) ) ) ;
250+ await expect . element ( page . getByLabelText ( "Comment on selected text" ) ) . toBeInViewport ( ) ;
251+ const input = textarea ( ) ! ;
252+ expect ( input . scrollTop + input . clientHeight ) . toBeGreaterThanOrEqual ( input . scrollHeight - 1 ) ;
253+ } ) ;
254+ } ) ;
0 commit comments