Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/5968-fix-carousel-without-react-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Changed

- attachment carousel no longer uses `react-film`, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552), in PR [#5968](https://github.com/microsoft/BotFramework-WebChat/pull/5968), by [@OEvgeny](https://github.com/OEvgeny)
97 changes: 97 additions & 0 deletions __tests__/html2/carousel/autoCenter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { waitFor } from '@testduet/wait-for';
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

createRoot(document.getElementById('webchat')).render(
createElement(ReactWebChat, { directLine, store, styleOptions: { hideSendBox: true } })
);

await pageConditions.uiConnected();

const createHeroCard = index => ({
content: {
buttons: [{ title: `Buy item ${index}`, type: 'imBack', value: `Buy item ${index}` }],
text: 'Price: $XXX.XX USD',
title: `Details about item ${index}`
},
contentType: 'application/vnd.microsoft.card.hero'
});

// GIVEN: A carousel with 4 hero cards, scrolled to the start.
await directLine.emulateIncomingActivity({
attachmentLayout: 'carousel',
attachments: [createHeroCard(1), createHeroCard(2), createHeroCard(3), createHeroCard(4)],
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(1);

const carouselLayout = document.querySelector('.carousel-layout');
const carouselFilmstrip = carouselLayout.querySelector('.carousel-filmstrip');
const cards = Array.from(carouselFilmstrip.querySelectorAll('[aria-roledescription="attachment"]'));

// Distance from the center of the card to the center of the filmstrip.
const getOffCenter = card => {
const cardRect = card.getBoundingClientRect();
const filmstripRect = carouselFilmstrip.getBoundingClientRect();

return (cardRect.left + cardRect.right - filmstripRect.left - filmstripRect.right) / 2;
};

await pageConditions.carouselSettled(carouselLayout);
expect(carouselFilmstrip.scrollLeft).toBe(0);

// WHEN: The mouse wheel scrolls the carousel to 40 pixels before the second card would be centered.
const filmstripRect = carouselFilmstrip.getBoundingClientRect();

await host.sendDevToolsCommand('Input.dispatchMouseEvent', {
deltaX: getOffCenter(cards[1]) - 40,
deltaY: 0,
type: 'mouseWheel',
x: (filmstripRect.left + filmstripRect.right) / 2,
y: (filmstripRect.top + filmstripRect.bottom) / 2
});

// Move the pointer away, so the flippers slide out.
await host.sendDevToolsCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x: 0, y: 0 });

// THEN: Scroll snapping centers the second card.
await waitFor(() => expect(Math.abs(getOffCenter(cards[1]))).toBeLessThan(1));
await pageConditions.carouselSettled(carouselLayout);

await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions __tests__/html2/carousel/flipperButton.focusVisible.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@

const getFocusIndicatorStyle = flipper => {
const { boxShadow, outlineColor, outlineOffset, outlineStyle, outlineWidth } = getComputedStyle(
flipper.querySelector('.react-film__flipper__body')
flipper.querySelector('.carousel-flipper__body')
);

return { boxShadow, outlineColor, outlineOffset, outlineStyle, outlineWidth };
};

// The flipper is shown when its body has slid into the center of the flipper button.
const getFlipperBodyOffset = flipper => {
const bodyRect = flipper.querySelector('.react-film__flipper__body').getBoundingClientRect();
const bodyRect = flipper.querySelector('.carousel-flipper__body').getBoundingClientRect();
const buttonRect = flipper.getBoundingClientRect();

return Math.round(bodyRect.left + bodyRect.width / 2 - (buttonRect.left + buttonRect.width / 2));
Expand All @@ -96,7 +96,7 @@

// GIVEN: The carousel is scrolled to the start, the "Previous" flipper is hidden.
expect(document.querySelector('.carousel-filmstrip').scrollLeft).toBe(0);
expect(previousFlipper.querySelector('.react-film__main__slider--hide')).toBeTruthy();
expect(previousFlipper.querySelector('.carousel-flipper__slider--at-edge')).toBeTruthy();

// WHEN: Focus on the first attachment of the carousel via keyboard.
pageObjects.focusTranscript();
Expand All @@ -105,7 +105,7 @@
// THEN: The first attachment should have the focus, both flippers should stay hidden.
await waitFor(() => expect(document.activeElement).toBe(pageElements.activityAttachments()[0][0]));
expect(getFlipperBodyOffset(nextFlipper)).toBe(50);
expect(previousFlipper.querySelector('.react-film__main__slider--hide')).toBeTruthy();
expect(previousFlipper.querySelector('.carousel-flipper__slider--at-edge')).toBeTruthy();

// THEN: The first card shows the focus indicator, no flipper is shown.
await host.snapshot('local');
Expand Down
98 changes: 98 additions & 0 deletions __tests__/html2/carousel/flipperButton.resize.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { waitFor } from '@testduet/wait-for';
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const webChatElement = document.getElementById('webchat');

await host.windowSize(640, 640, webChatElement);

const { directLine, store } = testHelpers.createDirectLineEmulator();

createRoot(webChatElement).render(
createElement(ReactWebChat, { directLine, store, styleOptions: { hideSendBox: true } })
);

await pageConditions.uiConnected();

const createHeroCard = index => ({
content: {
buttons: [{ title: `Buy item ${index}`, type: 'imBack', value: `Buy item ${index}` }],
text: 'Price: $XXX.XX USD',
title: `Details about item ${index}`
},
contentType: 'application/vnd.microsoft.card.hero'
});

// GIVEN: Web Chat is 640 pixels wide, with a carousel of 2 hero cards.
await directLine.emulateIncomingActivity({
attachmentLayout: 'carousel',
attachments: [createHeroCard(1), createHeroCard(2)],
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(1);

const carouselLayout = document.querySelector('.carousel-layout');
const getFlipperLabels = () =>
Array.from(carouselLayout.querySelectorAll('.carousel-flipper'), flipper =>
flipper.getAttribute('aria-label')
);

// THEN: Both cards fit, there are no flippers.
await pageConditions.carouselSettled(carouselLayout);
expect(getFlipperLabels()).toEqual([]);

await host.snapshot('local');

// WHEN: Web Chat is narrowed to 360 pixels, without moving the pointer.
await host.windowSize(360, 640, webChatElement);

// THEN: The cards overflow, the flippers are rendered (slid out until hovered), so the keyboard can reach them.
await waitFor(() => expect(getFlipperLabels()).toEqual(['Previous', 'Next']));
await pageConditions.carouselSettled(carouselLayout);

await host.snapshot('local');

// WHEN: The pointer moves over the carousel.
await host.hover(carouselLayout.querySelector('.carousel-filmstrip'));

// THEN: The "Next" flipper slides in, the "Previous" flipper stays out at the start.
const [previousFlipper, nextFlipper] = carouselLayout.querySelectorAll('.carousel-flipper');

await waitFor(() => expect(testHelpers.isFlipperShown(nextFlipper)).toBe(true));
expect(testHelpers.isFlipperHidden(previousFlipper)).toBe(true);

await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 changes: 109 additions & 0 deletions __tests__/html2/carousel/flipperButton.tabOrder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { waitFor } from '@testduet/wait-for';
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();
const dir = new URLSearchParams(location.search).get('dir') || 'ltr';

createRoot(document.getElementById('webchat')).render(
createElement(ReactWebChat, { dir, directLine, store, styleOptions: { hideSendBox: true } })
);

await pageConditions.uiConnected();

// GIVEN: A carousel with 4 hero cards.
await directLine.emulateIncomingActivity({
attachmentLayout: 'carousel',
attachments: [1, 2, 3, 4].map(index => ({
content: {
buttons: [{ title: `Buy item ${index}`, type: 'imBack', value: `Buy item ${index}` }],
text: 'Price: $XXX.XX USD',
title: `Details about item ${index}`
},
contentType: 'application/vnd.microsoft.card.hero'
})),
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(1);

const carouselLayout = document.querySelector('.carousel-layout');
const cards = Array.from(carouselLayout.querySelectorAll('[aria-roledescription="attachment"]'));

// GIVEN: Focus on the first card via keyboard, then press END.
pageObjects.focusTranscript();
await host.sendKeys('ENTER');
await host.sendKeys('END');

// THEN: The last card has focus, the carousel is scrolled to the end.
expect(document.activeElement).toBe(cards[3]);
await waitFor(() => expect(testHelpers.isCardInView(cards[3])).toBe(true));
await pageConditions.carouselSettled(carouselLayout);
await host.snapshot('local');

// WHEN: Press TAB.
await host.sendTab();

// THEN: The button in the last card has focus.
expect(document.activeElement).toBe(cards[3].querySelector('button'));
await pageConditions.carouselSettled(carouselLayout);
await host.snapshot('local');

// WHEN: Press TAB.
await host.sendTab();

// THEN: The "Previous" flipper has focus, it is at the inline-start side: left in LTR, right in RTL.
expect(document.activeElement).toBe(carouselLayout.querySelector('.carousel-flipper--previous'));
expect(document.activeElement.getAttribute('aria-label')).toBe('Previous');
await waitFor(() => expect(testHelpers.isFlipperShown(document.activeElement)).toBe(true));
await host.snapshot('local');

// WHEN: Press TAB.
await host.sendTab();

// THEN: The "Next" flipper has focus, it is at the inline-end side: right in LTR, left in RTL.
// The carousel is at the end, the flipper is shown only because it has keyboard focus.
expect(document.activeElement).toBe(carouselLayout.querySelector('.carousel-flipper--next'));
expect(document.activeElement.getAttribute('aria-label')).toBe('Next');
await waitFor(() => expect(testHelpers.isFlipperShown(document.activeElement)).toBe(true));

// The "Previous" flipper slides out 1 second after it lost focus.
await waitFor(
() =>
expect(testHelpers.isFlipperHidden(carouselLayout.querySelector('.carousel-flipper--previous'))).toBe(true),
{ timeout: 2000 }
);
await host.snapshot('local');
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions __tests__/html2/carousel/flipperButton.tabOrder.rtl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './flipperButton.tabOrder?dir=rtl';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions __tests__/html2/carousel/navigation.arrowKeys.button.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
const carouselLayout = document.querySelector('.carousel-layout');
const carouselFilmstrip = carouselLayout.querySelector('.carousel-filmstrip');
const cards = Array.from(carouselFilmstrip.querySelectorAll('[aria-roledescription="attachment"]'));
const flippers = Array.from(carouselLayout.querySelectorAll('.react-film__flipper'));
const flippers = Array.from(carouselLayout.querySelectorAll('.carousel-flipper'));
const secondCardButton = cards[1].querySelector('button');

const isCardInView = card => {
Expand All @@ -95,7 +95,7 @@

// The flipper is hidden when its body has slid out of the flipper button (1s delay after scroll stops, then 300ms slide).
const isFlipperHidden = flipper => {
const bodyRect = flipper.querySelector('.react-film__flipper__body').getBoundingClientRect();
const bodyRect = flipper.querySelector('.carousel-flipper__body').getBoundingClientRect();
const buttonRect = flipper.getBoundingClientRect();

return bodyRect.right <= buttonRect.left || bodyRect.left >= buttonRect.right;
Expand All @@ -104,7 +104,7 @@
const waitForCarouselSettled = () =>
waitFor(
() => {
expect(carouselLayout.querySelector('.react-film__main--scrolling')).toBeNull();
expect(carouselLayout.querySelector('.carousel-layout__main--scrolling')).toBeNull();
expect(flippers.every(isFlipperHidden)).toBe(true);
},
{ timeout: 5000 }
Expand Down
2 changes: 1 addition & 1 deletion __tests__/html2/carousel/navigation.tab.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<style type="text/css">
.react-film__flipper {
.carousel-flipper {
display: none;
}
</style>
Expand Down
Loading
Loading