Repository navigation
Render with createRoot() in renderWebChat() when ReactDOM.render() is not available #5963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
74a3041
bd5c77e
d57b101
9750e88
d2b9d87
0a88658
d9318f7
301f8e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ### Added | ||
|
|
||
| - `renderWebChat()`: added support of a root from `createRoot()` for React 19, related to [#5552](https://github.com/microsoft/BotFramework-WebChat/issues/5552) and [#5850](https://github.com/microsoft/BotFramework-WebChat/issues/5850), in PR [#5963](https://github.com/microsoft/BotFramework-WebChat/pull/5963), by [@OEvgeny](https://github.com/OEvgeny) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // Emulates "react-dom" of React 19, where `ReactDOM.render()` was removed, on top of React 18. | ||
| // Named exports are kept: react-film, bundled in the static build, imports `render` by name (spyip/react-film#110). | ||
| import ReactDOM from '/__dist__/packages/bundle/static/react-dom.js'; | ||
|
|
||
| export * from '/__dist__/packages/bundle/static/react-dom.js'; | ||
|
|
||
| const { render: _render, ...ReactDOMWithoutRender } = ReactDOM; | ||
|
|
||
| export default Object.freeze(ReactDOMWithoutRender); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <title>renderWebChat() renders into a custom element that has its own render() method (ESM build)</title> | ||
| <link href="/assets/index.css" rel="stylesheet" type="text/css" /> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <!-- Custom elements are inline by default. --> | ||
| <chat-container id="webchat" style="display: block"></chat-container> | ||
| </main> | ||
| <script type="importmap"> | ||
| { | ||
| "imports": { | ||
| "botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js", | ||
| "react": "/__dist__/packages/bundle/static/react.js", | ||
| "react-dom": "/__dist__/packages/bundle/static/react-dom.js" | ||
| } | ||
| } | ||
| </script> | ||
| <script type="module"> | ||
| import '/test-harness.mjs'; | ||
| import '/test-page-object.mjs'; | ||
|
|
||
| import { createStoreWithOptions, renderWebChat, testIds } from 'botframework-webchat'; | ||
| import { version } from 'react'; | ||
|
|
||
| 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(); | ||
|
|
||
| // GIVEN: React 18, which still has `ReactDOM.render()`. | ||
| expect(version).toMatch(/^18\./u); | ||
|
|
||
| // GIVEN: A custom element with its own `render()` method, like Lit components. | ||
| let numRenderCalls = 0; | ||
|
|
||
| customElements.define( | ||
| 'chat-container', | ||
| class extends HTMLElement { | ||
| render() { | ||
| numRenderCalls++; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| const chatContainer = document.getElementById('webchat'); | ||
|
|
||
| expect(typeof chatContainer.render).toBe('function'); | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() into the custom element, without the send box. | ||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, chatContainer); | ||
|
|
||
| // THEN: The custom element is treated as an element, not as a root: its `render()` is not called. | ||
| expect(numRenderCalls).toBe(0); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity('Hello, World!'); | ||
| await pageConditions.numActivitiesShown(1); | ||
|
|
||
| // THEN: Web Chat is rendered inside the custom element. | ||
| expect(chatContainer.contains(pageElements.transcript())).toBe(true); | ||
|
|
||
| // THEN: Web Chat shows the message without the send box. | ||
| await host.snapshot('local'); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <title>renderWebChat() renders with the root passed when ReactDOM.render() is unavailable (ESM build)</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 when https://github.com/spyip/react-film/pull/110 is released and bumped: the static build cannot 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" | ||
| } | ||
| } | ||
| </script> | ||
| <script type="module"> | ||
| import '/test-harness.mjs'; | ||
| import '/test-page-object.mjs'; | ||
|
|
||
| import { waitFor } from '@testduet/wait-for'; | ||
| import { createStoreWithOptions, renderWebChat, testIds } from 'botframework-webchat'; | ||
| import ReactDOM from 'react-dom'; | ||
| 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(); | ||
| const webChatElement = document.getElementById('webchat'); | ||
|
|
||
| // GIVEN: "react-dom" without `ReactDOM.render()`, like React 19. | ||
| expect(ReactDOM.render).toBeUndefined(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In #5971 ( |
||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() into the element. | ||
| // THEN: It throws, asking for a root: the page imports "react-dom/client", not Web Chat. | ||
| expect(() => renderWebChat({ directLine, store }, webChatElement)).toThrow( | ||
| 'renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' | ||
| ); | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() with a root from createRoot() of the page, without the send box. | ||
| const root = createRoot(webChatElement); | ||
|
|
||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, root); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity('Hello, World!'); | ||
| await pageConditions.numActivitiesShown(1); | ||
|
|
||
| expect(pageElements.sendBoxTextBox()).toBeFalsy(); | ||
|
|
||
| // THEN: Web Chat shows the message without the send box. | ||
| await host.snapshot('local'); | ||
|
|
||
| const transcript = pageElements.transcript(); | ||
|
|
||
| // WHEN: renderWebChat() is called again with the same root, with the send box. | ||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: false } }, root); | ||
|
|
||
| // THEN: The send box appears. | ||
| await waitFor(() => expect(pageElements.sendBoxTextBox()).toBeTruthy()); | ||
|
|
||
| // THEN: The same root is updated: the transcript is not mounted again. | ||
| expect(pageElements.transcript()).toBe(transcript); | ||
|
|
||
| // THEN: Web Chat shows the same message with the send box. | ||
| await host.snapshot('local'); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <title>React 19: renderWebChat() of the IIFE build renders with the root passed</title> | ||
| <link href="/assets/index.css" rel="stylesheet" type="text/css" /> | ||
| <script type="importmap"> | ||
| { | ||
| "imports": { | ||
| "@testduet/wait-for": "https://esm.sh/@testduet/wait-for", | ||
| "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 crossorigin="anonymous" src="/test-harness.js"></script> | ||
| <script crossorigin="anonymous" src="/test-page-object.js"></script> | ||
| <script type="module"> | ||
| // React 19 has no UMD build: the IIFE build of Web Chat uses `window.React` and `window.ReactDOM` when they are set. | ||
| import React from 'react'; | ||
| import ReactDOM from 'react-dom'; | ||
|
|
||
| window.React = React; | ||
| window.ReactDOM = ReactDOM; | ||
| </script> | ||
| <script crossorigin="anonymous" defer src="/__dist__/webchat-es5.js"></script> | ||
| </head> | ||
| <body> | ||
| <main id="webchat"></main> | ||
| <script type="module"> | ||
| import { waitFor } from '@testduet/wait-for'; | ||
| import { version } from 'react'; | ||
| import ReactDOM from 'react-dom'; | ||
| import { createRoot } from 'react-dom/client'; | ||
|
|
||
| run(async function () { | ||
| const { | ||
| WebChat: { renderWebChat } | ||
| } = window; // Imports in UMD fashion. | ||
|
|
||
| const { directLine, store } = testHelpers.createDirectLineEmulator(); | ||
| const webChatElement = document.getElementById('webchat'); | ||
|
|
||
| // GIVEN: React 19, which no longer has `ReactDOM.render()`. | ||
| expect(version).toBe('19.3.1'); | ||
| expect(ReactDOM.render).toBeUndefined(); | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() into the element. | ||
| // THEN: It throws, asking for a root: React 19 removed `ReactDOM.render()`. | ||
| expect(() => renderWebChat({ directLine, store }, webChatElement)).toThrow( | ||
| 'renderWebChat() on React 19 or later needs a root: pass createRoot(element) from "react-dom/client" instead of the element.' | ||
| ); | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() with something that is neither an element nor a root. | ||
| // THEN: It throws. | ||
| expect(() => renderWebChat({ directLine, store }, {})).toThrow( | ||
| 'renderWebChat() expects an HTMLElement or a root from createRoot().' | ||
| ); | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() with a root from createRoot() instead of the element, without the send box, as the error says. | ||
| const root = createRoot(webChatElement); | ||
|
|
||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, root); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity('Hello, World!'); | ||
| await pageConditions.numActivitiesShown(1); | ||
|
|
||
| expect(pageElements.sendBoxTextBox()).toBeFalsy(); | ||
|
|
||
| // THEN: Web Chat shows the message without the send box. | ||
| await host.snapshot('local'); | ||
|
|
||
| const transcript = pageElements.transcript(); | ||
|
|
||
| // WHEN: renderWebChat() is called again with the same root, with the send box. | ||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: false } }, root); | ||
|
|
||
| // THEN: The send box appears. | ||
| await waitFor(() => expect(pageElements.sendBoxTextBox()).toBeTruthy()); | ||
|
|
||
| // THEN: Web Chat is updated, not mounted again: the transcript is the same element. | ||
| expect(pageElements.transcript()).toBe(transcript); | ||
|
|
||
| // THEN: Web Chat shows the same message with the send box. | ||
| await host.snapshot('local'); | ||
|
|
||
| // WHEN: The root is unmounted. | ||
| root.unmount(); | ||
|
|
||
| // THEN: Web Chat is gone: it was rendered by the root passed. | ||
| expect(pageElements.transcript()).toBeFalsy(); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| <!doctype html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <title>React 18: renderWebChat() renders with the root passed instead of ReactDOM.render() (ESM build)</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": "/__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, renderWebChat, testIds } from 'botframework-webchat'; | ||
| import { version } from 'react'; | ||
| import ReactDOM from 'react-dom'; | ||
| 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(); | ||
|
|
||
| // GIVEN: React 18, which still has `ReactDOM.render()`. | ||
| expect(version).toMatch(/^18\./u); | ||
| expect(typeof ReactDOM.render).toBe('function'); | ||
|
|
||
| // GIVEN: A root from createRoot(), which records what it renders. | ||
| const root = createRoot(document.getElementById('webchat')); | ||
| const renderedChildren = []; | ||
| const recordingRoot = { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider use
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In #5971 ( |
||
| render(children) { | ||
| renderedChildren.push(children); | ||
| root.render(children); | ||
| } | ||
| }; | ||
|
|
||
| // WHEN: Web Chat is rendered by renderWebChat() with the root instead of the element, without the send box. | ||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: true } }, recordingRoot); | ||
|
|
||
| // THEN: The root rendered Web Chat once. | ||
| expect(renderedChildren).toHaveLength(1); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| await directLine.emulateIncomingActivity('Hello, World!'); | ||
| await pageConditions.numActivitiesShown(1); | ||
|
|
||
| expect(pageElements.sendBoxTextBox()).toBeFalsy(); | ||
|
|
||
| // THEN: Web Chat shows the message without the send box. | ||
| await host.snapshot('local'); | ||
|
|
||
| const transcript = pageElements.transcript(); | ||
|
|
||
| // WHEN: renderWebChat() is called again with the same root, with the send box. | ||
| renderWebChat({ directLine, store, styleOptions: { hideSendBox: false } }, recordingRoot); | ||
|
|
||
| // THEN: The root rendered Web Chat again. | ||
| expect(renderedChildren).toHaveLength(2); | ||
|
|
||
| // THEN: The send box appears. | ||
| await waitFor(() => expect(pageElements.sendBoxTextBox()).toBeTruthy()); | ||
|
|
||
| // THEN: Web Chat is updated, not mounted again: the transcript is the same element. | ||
| expect(pageElements.transcript()).toBe(transcript); | ||
|
|
||
| // WHEN: The root is unmounted. | ||
| root.unmount(); | ||
|
|
||
| // THEN: Web Chat is gone: it was rendered by the root, not by `ReactDOM.render()`. | ||
| expect(pageElements.transcript()).toBeFalsy(); | ||
| }); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import coreRenderWebChat from '../../../renderWebChat'; | ||
| import coreRenderWebChat, { type RenderWebChat } from '../../../renderWebChat'; | ||
| import { ReactWebChat } from '../component/full'; | ||
|
|
||
| const renderWebChat = coreRenderWebChat.bind(null, ReactWebChat); | ||
| const renderWebChat: RenderWebChat = coreRenderWebChat.bind(null, ReactWebChat); | ||
|
|
||
| export { renderWebChat }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import coreRenderWebChat from '../../../renderWebChat'; | ||
| import coreRenderWebChat, { type RenderWebChat } from '../../../renderWebChat'; | ||
| import { ReactWebChat } from '../component/minimal'; | ||
|
|
||
| const renderWebChat = coreRenderWebChat.bind(null, ReactWebChat); | ||
| const renderWebChat: RenderWebChat = coreRenderWebChat.bind(null, ReactWebChat); | ||
|
|
||
| export { renderWebChat }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use
esm.sh/react@19.3.1as well?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried: the static build fails with "'react-dom' does not provide an export named 'render'", from react-film's
import { render } from 'react-dom'. Real React 19 with the static build is tested on the react-film stack (react19/esm.renderWebChat.html); this page switches once react-film is gone (TODO updated in #5971).