Skip to content

Commit d2eed9f

Browse files
committed
feat: add navigation tests
1 parent 8608112 commit d2eed9f

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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

Comments
 (0)