@@ -11,24 +11,27 @@ function createScrollFixture() {
1111 parentElement : null as HTMLElement | null ,
1212 } as unknown as HTMLElement ;
1313
14+ const scrollCalls : ScrollToOptions [ ] = [ ] ;
1415 const scroll = {
15- style : { overflowY : "auto" } ,
16+ style : { overflowY : "auto" , scrollBehavior : "" } ,
1617 scrollTop : 0 ,
1718 scrollHeight : 400 ,
18- scrollTo ( { top } : ScrollToOptions ) {
19+ scrollTo ( options : ScrollToOptions ) {
20+ scrollCalls . push ( options ) ;
21+ const { top } = options ;
1922 this . scrollTop = top ?? 0 ;
2023 } ,
2124 parentElement : null ,
2225 } as unknown as HTMLElement ;
2326
2427 content . parentElement = scroll ;
2528 const root = {
26- querySelector ( selector : string ) {
27- return selector === '[data-testid="copilot-scroll-content"]' ? content : null ;
29+ querySelectorAll ( selector : string ) {
30+ return selector === '[data-testid="copilot-scroll-content"]' ? [ content ] : [ ] ;
2831 } ,
2932 } as unknown as ParentNode ;
3033
31- return { root, scroll } ;
34+ return { root, scroll, scrollCalls } ;
3235}
3336
3437describe ( "chat scroll helpers" , ( ) => {
@@ -43,6 +46,67 @@ describe("chat scroll helpers", () => {
4346 expect ( scroll . scrollTop ) . toBe ( scroll . scrollHeight ) ;
4447 } ) ;
4548
49+ it ( "uses instant scroll even when the container has smooth scroll behavior" , ( ) => {
50+ const { root, scroll, scrollCalls } = createScrollFixture ( ) ;
51+ scroll . style . scrollBehavior = "smooth" ;
52+
53+ expect ( scrollCopilotChatToBottom ( root ) ) . toBe ( true ) ;
54+ expect ( scroll . scrollTop ) . toBe ( scroll . scrollHeight ) ;
55+ expect ( scrollCalls ) . toEqual ( [ { top : scroll . scrollHeight , behavior : "auto" } ] ) ;
56+ expect ( scroll . style . scrollBehavior ) . toBe ( "smooth" ) ;
57+ } ) ;
58+
59+ it ( "skips hidden chat sessions when finding the active scroll container" , ( ) => {
60+ const hiddenContent = {
61+ dataset : { testid : "copilot-scroll-content" } ,
62+ parentElement : null as HTMLElement | null ,
63+ } as unknown as HTMLElement ;
64+ const hiddenScroll = {
65+ style : { overflowY : "auto" , display : "none" } ,
66+ parentElement : null ,
67+ } as unknown as HTMLElement ;
68+ hiddenContent . parentElement = hiddenScroll ;
69+
70+ const { scroll, root } = createScrollFixture ( ) ;
71+ const scopedRoot = {
72+ querySelectorAll ( selector : string ) {
73+ return selector === '[data-testid="copilot-scroll-content"]'
74+ ? [ hiddenContent , ...( root . querySelectorAll ( selector ) as unknown as HTMLElement [ ] ) ]
75+ : [ ] ;
76+ } ,
77+ } as unknown as ParentNode ;
78+
79+ expect ( findCopilotChatScrollContainer ( scopedRoot ) ) . toBe ( scroll ) ;
80+ } ) ;
81+
82+ it ( "prefers the ancestor that actually scrolls over a non-scrolling overflow wrapper" , ( ) => {
83+ const content = {
84+ dataset : { testid : "copilot-scroll-content" } ,
85+ parentElement : null as HTMLElement | null ,
86+ } as unknown as HTMLElement ;
87+ const innerOverflow = {
88+ style : { overflowY : "auto" } ,
89+ scrollHeight : 800 ,
90+ clientHeight : 800 ,
91+ parentElement : null as HTMLElement | null ,
92+ } as unknown as HTMLElement ;
93+ const scrollViewport = {
94+ style : { overflowY : "auto" } ,
95+ scrollHeight : 800 ,
96+ clientHeight : 300 ,
97+ parentElement : null ,
98+ } as unknown as HTMLElement ;
99+ content . parentElement = innerOverflow ;
100+ innerOverflow . parentElement = scrollViewport ;
101+ const root = {
102+ querySelectorAll ( selector : string ) {
103+ return selector === '[data-testid="copilot-scroll-content"]' ? [ content ] : [ ] ;
104+ } ,
105+ } as unknown as ParentNode ;
106+
107+ expect ( findCopilotChatScrollContainer ( root ) ) . toBe ( scrollViewport ) ;
108+ } ) ;
109+
46110 it ( "retries scrolling until attempts are exhausted" , ( ) => {
47111 const { root, scroll } = createScrollFixture ( ) ;
48112 const calls : number [ ] = [ ] ;
0 commit comments