Skip to content
Open
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
13 changes: 13 additions & 0 deletions .changelog/5955-fix-react-19-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Added

- Added [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), support for React 19, in PR [#5955](https://github.com/microsoft/BotFramework-WebChat/pull/5955), by [@OEvgeny](https://github.com/OEvgeny)
- 👷🏻 Pull request validation: added type definitions tests with `@types/react@19`, in PR [#5955](https://github.com/microsoft/BotFramework-WebChat/pull/5955), by [@OEvgeny](https://github.com/OEvgeny)

### Removed

- Components: removed the remaining `propTypes` and the `prop-types` dependency, React 19 ignores them, in PR [#5955](https://github.com/microsoft/BotFramework-WebChat/pull/5955), by [@OEvgeny](https://github.com/OEvgeny)

### Fixed

- Fixed Adaptive Cards submitting empty inputs in Strict Mode on React 19, in PR [#5955](https://github.com/microsoft/BotFramework-WebChat/pull/5955), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed `Composer` of the full bundle not calling a `children` function with `extraStyleSet`, in PR [#5955](https://github.com/microsoft/BotFramework-WebChat/pull/5955), by [@OEvgeny](https://github.com/OEvgeny)
9 changes: 9 additions & 0 deletions .github/workflows/pull-request-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ jobs:
run: ../../node_modules/.bin/tsd -t ./dist/botframework-webchat-core.d.mts
working-directory: ./packages/core

# The steps above use @types/react@16 (our baseline), check with the types of the latest React too.
# In every workspace: packages that declare @types/react@16 would otherwise keep their own copy.
- name: Install @types/react@19 and @types/react-dom@19
run: npm install --no-save --workspaces --include-workspace-root @types/react@19 @types/react-dom@19

- name: Run tsd under packages/bundle with @types/react@19
run: ../../node_modules/.bin/tsd -t ./dist/botframework-webchat.d.mts
working-directory: ./packages/bundle

bundler-test:
name: Bundler test (${{ matrix.bundler }}, React ${{ matrix.react }})
needs:
Expand Down
9 changes: 0 additions & 9 deletions __tests__/html2/react19/asset/reactDOMWithoutRender.js

This file was deleted.

16 changes: 6 additions & 10 deletions __tests__/html2/react19/renderWebChat.esm.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
<!doctype html>
<html lang="en-US">
<head>
<title>renderWebChat() renders with the root passed when ReactDOM.render() is unavailable (ESM build)</title>
<title>React 19: renderWebChat() of the ESM build renders with the root passed</title>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<!--
React 18 with "react-dom" emulating React 19 (no `ReactDOM.render()`).
TODO: Use React 19 once the static build no longer bundles react-film, which imports `render` from "react-dom" (removed, or https://github.com/spyip/react-film/pull/110 released and bumped): it fails to load with React 19 until then.
-->
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "./asset/reactDOMWithoutRender.js",
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
"react": "https://esm.sh/react@19.3.1?dev",
"react-dom": "https://esm.sh/react-dom@19.3.1?dev",
"react-dom/client": "https://esm.sh/react-dom@19.3.1/client?dev"
}
}
</script>
Expand All @@ -37,8 +33,8 @@
const { directLine, store } = testHelpers.createDirectLineEmulator();
const webChatElement = document.getElementById('webchat');

// GIVEN: "react-dom" of React 18 without `ReactDOM.render()`, like React 19.
expect(ReactDOM.version).toEqual(expect.stringMatching(/^18\./u)); // React 18 underneath the emulation, see the import map.
// GIVEN: React 19, which no longer has `ReactDOM.render()`.
expect(ReactDOM.version).toBe('19.3.1');
expect(ReactDOM.render).toBeUndefined();

// WHEN: Web Chat is rendered by renderWebChat() into the element.
Expand Down
98 changes: 98 additions & 0 deletions __tests__/html2/react19/strictMode.adaptiveCard.submit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<!doctype html>
<html lang="en-US">
<head>
<title>React 19: submits the typed value of an Adaptive Card in Strict Mode</title>
<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",
"react": "https://esm.sh/react@19.3.1?dev",
"react-dom": "https://esm.sh/react-dom@19.3.1?dev",
"react-dom/client": "https://esm.sh/react-dom@19.3.1/client?dev"
}
}
</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, StrictMode, version } from 'react';
import { createRoot } from 'react-dom/client';

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

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

// GIVEN: The development build of React 19, which calls `useMemo()` twice in Strict Mode and keeps the first result.
expect(version).toBe('19.3.1');

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

await pageConditions.uiConnected();

// GIVEN: A card with a text input and a "Submit" button without `data`, which sends the inputs as a post back.
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
actions: [{ title: 'Submit', type: 'Action.Submit' }],
body: [{ id: 'firstName', label: 'First name', type: 'Input.Text' }],
type: 'AdaptiveCard',
version: '1.3'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(1);

const input = document.querySelector('.ac-textInput input, input.ac-textInput');
const submitButton = document.querySelector('.ac-pushButton');

expect(submitButton).toHaveProperty('textContent', 'Submit');

// WHEN: "John" is typed into the input and the "Submit" button is clicked.
await host.click(input);
await host.sendKeys('John');

const { activity, resolveAll } = await directLine.actPostActivity(() => host.click(submitButton));

await resolveAll();

// THEN: The post back should have the typed value.
expect(activity).toHaveProperty('value', { firstName: 'John' });

// THEN: The input on screen should be the one the card read.
expect(document.querySelector('.ac-textInput input, input.ac-textInput')).toBe(input);
expect(input).toHaveProperty('value', 'John');

// Clicking "Submit" selects it, after the post back.
await waitFor(async () =>
expect((await testHelpers.getAccessibilityNode(submitButton)).description?.value).toBe('Selected')
);

// THEN: The card shows "John" in the input.
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.
198 changes: 198 additions & 0 deletions __tests__/html2/react19/strictMode.esm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!doctype html>
<html lang="en-US">
<head>
<title>React 19 in Strict Mode (ESM build): message, carousel, suggested action and Adaptive Card</title>
<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",
"react": "https://esm.sh/react@19.3.1?dev",
"react-dom": "https://esm.sh/react-dom@19.3.1?dev",
"react-dom/client": "https://esm.sh/react-dom@19.3.1/client?dev"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

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

run(async function () {
// TODO: This is for `createDirectLineEmulator` and `pageElements` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions, testIds };

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

const MountCounter = () => {
useEffect(() => {
numMounts++;
}, []);

return null;
};

// GIVEN: The development build of React 19, which runs the checks of Strict Mode.
expect(version).toBe('19.3.1');

// WHEN: Web Chat is rendered in Strict Mode.
createRoot(document.getElementById('webchat')).render(
createElement(StrictMode, {}, createElement(MountCounter), createElement(ReactWebChat, { directLine, store }))
);

// THEN: Strict Mode should mount, unmount, and mount again.
await waitFor(() => expect(numMounts).toBe(2));

await pageConditions.uiConnected();

// WHEN: The user types a message and presses ENTER.
const { activity: typedActivity, resolveAll: resolveTypedActivity } = await directLine.actPostActivity(
async () => {
await host.click(pageElements.sendBoxTextBox());
await host.sendKeys('Hello, World!', 'ENTER');

await pageConditions.numActivitiesShown(1);
}
);

await resolveTypedActivity();

expect(typedActivity).toHaveProperty('text', 'Hello, World!');

const createHeroCard = index => ({
content: { title: `Item ${index}` },
contentType: 'application/vnd.microsoft.card.hero'
});

// WHEN: The bot replies with a carousel of 3 cards and a suggested action.
await directLine.emulateIncomingActivity({
attachmentLayout: 'carousel',
attachments: [createHeroCard(1), createHeroCard(2), createHeroCard(3)],
suggestedActions: { actions: [{ title: 'USA', type: 'imBack', value: 'USA' }] },
text: 'Where are you from?'
});

await pageConditions.numActivitiesShown(2);
await pageConditions.suggestedActionsShown();

const carouselLayout = document.querySelector('.carousel-layout');
const carouselFilmstrip = carouselLayout.querySelector('.carousel-filmstrip');
const flippers = Array.from(carouselLayout.querySelectorAll('.react-film__flipper'));

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

return bodyRect.right <= buttonRect.left || bodyRect.left >= buttonRect.right;
};

const waitForCarouselSettled = () =>
waitFor(
() => {
expect(carouselLayout.querySelector('.react-film__main--scrolling')).toBeNull();
expect(flippers.every(isFlipperHidden)).toBe(true);
},
{ timeout: 5000 }
);

expect(carouselFilmstrip.scrollLeft).toBe(0);

await waitForCarouselSettled();
await pageConditions.scrollStabilized();

// THEN: "Hello, World!" is sent, the bot shows the carousel at its first card and the "USA" suggested action.
await host.snapshot('local');

// WHEN: The "Next" flipper of the carousel is clicked, then the mouse leaves the carousel.
await host.click(carouselLayout.querySelector('[aria-label="Next"]'));
await waitFor(() => expect(carouselFilmstrip.scrollLeft).toBeGreaterThan(0));
await host.hover(pageElements.sendBoxTextBox());

await waitForCarouselSettled();

// THEN: The carousel is scrolled away from its first card.
await host.snapshot('local');

const carouselScrollLeft = carouselFilmstrip.scrollLeft;

// WHEN: The user clicks the suggested action.
const { activity: clickedActivity, resolveAll: resolveClickedActivity } = await directLine.actPostActivity(
async () => {
await pageObjects.clickNthSuggestedAction(1);
await pageConditions.numActivitiesShown(3);
}
);

await resolveClickedActivity();

expect(clickedActivity).toHaveProperty('text', 'USA');

await waitFor(() => expect(pageElements.suggestedActions()).toHaveLength(0));
await pageConditions.scrollStabilized();

// The carousel keeps its position.
expect(document.querySelector('.carousel-layout .carousel-filmstrip').scrollLeft).toBe(carouselScrollLeft);

// THEN: "USA" is sent and the suggested action is gone.
await host.snapshot('local');

// WHEN: The bot sends an Adaptive Card with a text input and a "Submit" button without `data`, which sends the inputs as a post back.
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
actions: [{ title: 'Submit', type: 'Action.Submit' }],
body: [{ id: 'firstName', label: 'First name', type: 'Input.Text' }],
type: 'AdaptiveCard',
version: '1.3'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
from: { role: 'bot' },
type: 'message'
});

await pageConditions.numActivitiesShown(4);

const input = document.querySelector('.ac-textInput input, input.ac-textInput');
const submitButton = document.querySelector('.ac-pushButton');

// WHEN: "John" is typed into the input and the "Submit" button is clicked.
await host.click(input);
await host.sendKeys('John');

const { activity: submittedActivity, resolveAll: resolveSubmittedActivity } = await directLine.actPostActivity(
() => host.click(submitButton)
);

await resolveSubmittedActivity();

// THEN: The post back should have the typed value.
expect(submittedActivity).toHaveProperty('value', { firstName: 'John' });

// Clicking "Submit" selects it, after the post back.
await waitFor(async () =>
expect((await testHelpers.getAccessibilityNode(submitButton)).description?.value).toBe('Selected')
);

await pageConditions.scrollStabilized();

// THEN: The card shows "John" in the input.
// `run()` ends with `host.done()`, which fails on any `console.error`, where React 19 logs its development warnings.
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.
Loading
Loading