1+ <!doctype html>
2+ < html lang ="en-US ">
3+ < head >
4+ < title > Part grouping: navigation</ title >
5+ < link href ="/assets/index.css " rel ="stylesheet " type ="text/css " />
6+ < script type ="importmap ">
7+ {
8+ "imports" : {
9+ "react" : "https://esm.sh/react@18.3.1" ,
10+ "react-dom" : "https://esm.sh/react-dom@18.3.1" ,
11+ "react-dom/" : "https://esm.sh/react-dom@18.3.1/" ,
12+ "@testduet/wait-for" : "https://unpkg.com/@testduet/wait-for@main/dist/wait-for.mjs" ,
13+ "@fluentui/react-components" : "https://esm.sh/@fluentui/react-components?deps=react@18.3.1&exports=FluentProvider,createDarkTheme,webLightTheme"
14+ }
15+ }
16+ </ script >
17+ < script crossorigin ="anonymous " src ="/test-harness.js "> </ script >
18+ < script crossorigin ="anonymous " src ="/test-page-object.js "> </ script >
19+ < script type ="module ">
20+ import React from 'react' ;
21+ window . React = React ;
22+ </ script >
23+ < script defer crossorigin ="anonymous " src ="/__dist__/webchat-es5.js "> </ script >
24+ < script defer crossorigin ="anonymous "src ="/__dist__/botframework-webchat-fluent-theme.development.js "> </ script >
25+ < style type ="text/css ">
26+ # webchat {
27+ width : 640px ;
28+ }
29+
30+ .fui-FluentProvider {
31+ height : 100% ;
32+ }
33+
34+ .theme .variant-copilot {
35+ --webchat__color--surface : var (--colorGrey98 );
36+ }
37+
38+ # key-history {
39+ align-items : center;
40+ display : flex;
41+ gap : 0.5em ;
42+ left : 0.5em ;
43+ padding : 0.5em ;
44+ position : fixed;
45+ top : 0.5em ;
46+ z-index : 1 ;
47+ font-size : 3rem ;
48+ }
49+
50+ # key-history kbd {
51+ background-color : # f7f7f7 ;
52+ border : 1px solid # ccc ;
53+ border-radius : 6px ;
54+ box-shadow : 0 1px 0 # aaaaaa44, 0 2px 0 # ffffff44 inset;
55+ color : # 222 ;
56+ font-weight : bold;
57+ padding : 0.2em 0.6em ;
58+ text-align : center;
59+ }
60+ # key-history kbd : last-of-type {
61+ background-color : # e4e8ec ;
62+ }
63+ </ style >
64+ </ head >
65+
66+ < body >
67+ < div id ="key-history " aria-hidden ="true "> </ div >
68+ < main id ="webchat "> </ main >
69+ < script type ="module ">
70+ import React from 'react' ;
71+ import { createRoot } from 'react-dom/client' ;
72+ import { FluentProvider , createDarkTheme , webLightTheme } from '@fluentui/react-components' ;
73+ import { waitFor } from '@testduet/wait-for' ;
74+
75+ const KEY_MAP = {
76+ ArrowDown : '↓' ,
77+ ArrowUp : '↑'
78+ } ;
79+
80+ const keyHistory = [ ] ;
81+ const keyHistoryElement = document . getElementById ( 'key-history' ) ;
82+
83+ const updateKeyHistory = ( ) => {
84+ const children = keyHistory . slice ( - 3 ) . map ( key => {
85+ const kbd = document . createElement ( 'kbd' ) ;
86+
87+ kbd . textContent = key ;
88+ return kbd ;
89+ } ) ;
90+
91+ keyHistoryElement . replaceChildren ( ...children ) ;
92+ } ;
93+
94+ window . addEventListener (
95+ 'keydown' ,
96+ event => {
97+ keyHistory . push ( KEY_MAP [ event . key ] || event . key ) ;
98+ updateKeyHistory ( ) ;
99+ } ,
100+ { capture : true }
101+ ) ;
102+
103+ updateKeyHistory ( ) ;
104+
105+ run ( async function ( ) {
106+ const {
107+ WebChat : { FluentThemeProvider, ReactWebChat }
108+ } = window ;
109+
110+ const { directLine, store } = testHelpers . createDirectLineEmulator ( ) ;
111+
112+ const searchParams = new URLSearchParams ( location . search ) ;
113+ const variant = searchParams . get ( 'variant' ) ;
114+ const theme = searchParams . get ( 'fluent-theme' ) ;
115+
116+ await host . windowSize ( 640 , 1024 , document . getElementById ( 'webchat' ) ) ;
117+ await host . sendDevToolsCommand ( 'Emulation.setEmulatedMedia' , {
118+ features : [ { name : 'prefers-reduced-motion' , value : 'reduce' } ]
119+ } ) ;
120+
121+ const root = createRoot ( document . getElementById ( 'webchat' ) ) ;
122+
123+ let fluentTheme ;
124+ let codeBlockTheme ;
125+
126+ if ( theme === 'dark' || ( window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches && theme !== 'light' ) ) {
127+ fluentTheme = createDarkTheme ( { brand : { 100 : '#5661d4' } } ) ;
128+ codeBlockTheme = 'github-dark-default' ;
129+ } else {
130+ fluentTheme = webLightTheme ;
131+ codeBlockTheme = 'github-light-default' ;
132+ }
133+
134+ if ( variant ) {
135+ window . checkAccessibility = async ( ) => { } ;
136+ }
137+
138+ const webChatProps = { directLine, store, styleOptions : { codeBlockTheme } } ;
139+
140+ root . render (
141+ variant === 'copilot' || variant === 'fluent'
142+ ? React . createElement (
143+ FluentProvider ,
144+ { className : 'fui-FluentProvider' , theme : fluentTheme } ,
145+ React . createElement (
146+ FluentThemeProvider ,
147+ { variant : variant } ,
148+ React . createElement ( ReactWebChat , webChatProps )
149+ )
150+ )
151+ : React . createElement ( ReactWebChat , webChatProps )
152+ ) ;
153+
154+ await pageConditions . uiConnected ( ) ;
155+
156+ const createActivity = ( id , text , groupId ) => ( {
157+ entities : [
158+ {
159+ '@context' : 'https://schema.org' ,
160+ '@id' : '' ,
161+ '@type' : 'Message' ,
162+ abstract : text ,
163+ author : { name : 'Research' } ,
164+ isPartOf : { '@id' : groupId , '@type' : 'HowTo' } ,
165+ keywords : [ 'AIGeneratedContent' , 'AnalysisMessage' ] ,
166+ position : parseInt ( id . split ( '-' ) . pop ( ) , 10 ) ,
167+ type : 'https://schema.org/Message'
168+ }
169+ ] ,
170+ id,
171+ type : 'message'
172+ } ) ;
173+
174+ directLine . emulateIncomingActivity ( { from : { role : 'user' } , text : `Message from user.` , type : 'message' } ) ;
175+ await pageConditions . numActivitiesShown ( 1 ) ;
176+
177+ // Setup: Send all activities.
178+ directLine . emulateIncomingActivity ( createActivity ( 'activity-1' , 'Activity 1' ) ) ;
179+ directLine . emulateIncomingActivity ( createActivity ( 'g1-activity-1' , 'Group 1, Activity 1' , 'g-00001' ) ) ;
180+ directLine . emulateIncomingActivity ( createActivity ( 'g1-activity-2' , 'Group 1, Activity 2' , 'g-00001' ) ) ;
181+ directLine . emulateIncomingActivity ( createActivity ( 'g2-activity-1' , 'Group 2, Activity 1' , 'g-00002' ) ) ;
182+ directLine . emulateIncomingActivity ( createActivity ( 'g2-activity-2' , 'Group 2, Activity 2' , 'g-00002' ) ) ;
183+ directLine . emulateIncomingActivity ( createActivity ( 'activity-2' , 'Activity 2' ) ) ;
184+
185+ await pageConditions . numActivitiesShown ( 7 ) ;
186+ await pageObjects . focusTranscript ( ) ;
187+
188+ const activities = pageElements . activities ( ) ;
189+
190+ // Test Case 1: Expanded group navigation.
191+ // When: Navigating up from the last activity.
192+ await host . snapshot ( 'local' ) ;
193+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 1 ) ) ;
194+
195+ await host . sendKeys ( 'ARROW_UP' ) ; // activity-2 -> g2-activity-2
196+ await host . snapshot ( 'local' ) ;
197+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 2 ) ) ;
198+
199+ await host . sendKeys ( 'ARROW_UP' ) ; // g2-activity-2 -> g2-activity-1
200+ await host . snapshot ( 'local' ) ;
201+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 3 ) ) ;
202+
203+ await host . sendKeys ( 'ARROW_UP' ) ; // g2-activity-1 -> group 2 header
204+ await host . snapshot ( 'local' ) ;
205+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
206+
207+ await host . sendKeys ( 'ARROW_UP' ) ; // group 2 header -> g1-activity-2
208+ await host . snapshot ( 'local' ) ;
209+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 4 ) ) ;
210+
211+ // When: Navigating down.
212+ await host . sendKeys ( 'ARROW_DOWN' ) ; // g1-activity-2 -> group 2 header
213+ await host . snapshot ( 'local' ) ;
214+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
215+
216+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 2 header -> g2-activity-1
217+ await host . snapshot ( 'local' ) ;
218+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 3 ) ) ;
219+
220+ // Test Case 2: Collapsed group navigation.
221+ // When: Collapsing group 1 and group 2.
222+ const groupHeaders = ( ) => document . querySelectorAll ( '.collapsible-grouping__header .webchat__activity-button' ) ;
223+ await waitFor ( async ( ) => expect ( groupHeaders ( ) ) . toHaveLength ( 2 ) ) ;
224+
225+ for ( const header of groupHeaders ( ) ) {
226+ header . click ( ) ;
227+ }
228+ await waitFor ( async ( ) => expect ( document . querySelectorAll ( 'button[aria-expanded="false"]' ) ) . toHaveLength ( 2 ) ) ;
229+ await pageObjects . focusTranscript ( ) ;
230+
231+ // Then: Focus should be on the last activity.
232+ await host . snapshot ( 'local' ) ;
233+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 3 ) ) ;
234+
235+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 2 header -> activity-2
236+ await host . snapshot ( 'local' ) ;
237+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 1 ) ) ;
238+
239+ // When: Navigating up.
240+ await host . sendKeys ( 'ARROW_UP' ) ; // activity-2 -> group 2 header
241+ await host . snapshot ( 'local' ) ;
242+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
243+
244+ await host . sendKeys ( 'ARROW_UP' ) ; // group 2 header -> group 1 header
245+ await host . snapshot ( 'local' ) ;
246+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
247+
248+ await host . sendKeys ( 'ARROW_UP' ) ; // group 1 header -> activity-1
249+ await host . snapshot ( 'local' ) ;
250+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 6 ) ) ;
251+
252+ // When: Navigating down.
253+ await host . sendKeys ( 'ARROW_DOWN' ) ; // activity-1 -> group 1 header
254+ await host . snapshot ( 'local' ) ;
255+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
256+
257+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 1 header -> group 2 header
258+ await host . snapshot ( 'local' ) ;
259+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
260+
261+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 2 header -> activity-2
262+ await host . snapshot ( 'local' ) ;
263+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 1 ) ) ;
264+
265+ // Test Case 3: Mixed group navigation.
266+ // When: Expanding group 1, keeping group 2 collapsed.
267+ const collapsedGroupHeaders = ( ) => document . querySelectorAll ( 'button[aria-expanded="false"]' ) ;
268+ await waitFor ( async ( ) => expect ( collapsedGroupHeaders ( ) ) . toHaveLength ( 2 ) ) ;
269+ collapsedGroupHeaders ( ) [ 0 ] . click ( ) ; // Expand group 1.
270+ await waitFor ( async ( ) => expect ( document . querySelectorAll ( 'button[aria-expanded="true"]' ) ) . toHaveLength ( 1 ) ) ;
271+ await pageObjects . focusTranscript ( ) ;
272+
273+ // Then: Focus should be on the last activity.
274+ await host . snapshot ( 'local' ) ;
275+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 1 ) ) ;
276+
277+ // When: Navigating up.
278+ await host . sendKeys ( 'ARROW_UP' ) ; // activity-2 -> group 2 header (collapsed)
279+ await host . snapshot ( 'local' ) ;
280+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
281+
282+ await host . sendKeys ( 'ARROW_UP' ) ; // group 2 header -> g1-activity-2 (expanded)
283+ await host . snapshot ( 'local' ) ;
284+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 4 ) ) ;
285+
286+ await host . sendKeys ( 'ARROW_UP' ) ; // g1-activity-2 -> g1-activity-1
287+ await host . snapshot ( 'local' ) ;
288+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 5 ) ) ;
289+
290+ await host . sendKeys ( 'ARROW_UP' ) ; // g1-activity-1 -> group 1 header
291+ await host . snapshot ( 'local' ) ;
292+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
293+
294+ await host . sendKeys ( 'ARROW_UP' ) ; // group 1 header -> activity-1
295+ await host . snapshot ( 'local' ) ;
296+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 6 ) ) ;
297+
298+ // When: Navigating down.
299+ await host . sendKeys ( 'ARROW_DOWN' ) ; // activity-1 -> group 1 header
300+ await host . snapshot ( 'local' ) ;
301+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
302+
303+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 1 header -> g1-activity-1
304+ await host . snapshot ( 'local' ) ;
305+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 5 ) ) ;
306+
307+ await host . sendKeys ( 'ARROW_DOWN' ) ; // g1-activity-1 -> g1-activity-2
308+ await host . snapshot ( 'local' ) ;
309+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 4 ) ) ;
310+ await host . sendKeys ( 'ARROW_DOWN' ) ; // g1-activity-2 -> group 2 header (collapsed)
311+ await host . snapshot ( 'local' ) ;
312+ expect ( pageElements . focusedActivity ( ) ) . toBe ( undefined ) ;
313+ await host . sendKeys ( 'ARROW_DOWN' ) ; // group 2 header -> activity-2
314+ await host . snapshot ( 'local' ) ;
315+ expect ( pageElements . focusedActivity ( ) ) . toBe ( activities . at ( - 1 ) ) ;
316+ } ) ;
317+ </ script >
318+ </ body >
319+ </ html >
0 commit comments