Skip to content
Merged
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/6047-fix-activity-styles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Fixed styles lost after a React 19.2 `<Activity>` hides and shows Web Chat, in PR [#6047](https://github.com/microsoft/BotFramework-WebChat/pull/6047), by [@OEvgeny](https://github.com/OEvgeny)
80 changes: 80 additions & 0 deletions __tests__/html2/basic/twoWebChats.unmountOne.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Two Web Chats share their styles: the one left keeps them when the other is unmounted</title>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<style>
#webchat > div {
height: 50%;
}
</style>
</head>
<body>
<main id="webchat">
<div id="webchat-1"></div>
<div id="webchat-2"></div>
</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, ReactWebChat, testIds } from 'botframework-webchat';
import { createElement } 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 };

// `pageConditions.numActivitiesShown()` only counts the first transcript.
const getNumActivities = () => document.querySelectorAll('.webchat__basic-transcript__activity').length;
// The <style> elements of the styles the two Web Chats share.
const getNumSharedStyleElements = () =>
document.head.querySelectorAll('style[data-emotion^="webchat--css-"]').length;

// GIVEN: Two Web Chats with the same nonce and styles root (both default), so they share their styles.
// The first one hides its send box: two unnamed send box landmarks would not be unique.
const root1 = createRoot(document.getElementById('webchat-1'));
const webChat1 = testHelpers.createDirectLineEmulator();
const webChat2 = testHelpers.createDirectLineEmulator();

root1.render(createElement(ReactWebChat, { ...webChat1, styleOptions: { hideSendBox: true } }));
createRoot(document.getElementById('webchat-2')).render(createElement(ReactWebChat, webChat2));

await pageConditions.uiConnected();

await webChat1.directLine.emulateIncomingActivity('Hello from the first Web Chat.');
await webChat2.directLine.emulateIncomingActivity('Hello from the second Web Chat.');
await waitFor(() => expect(getNumActivities()).toBe(2));

expect(getNumSharedStyleElements()).toBe(1);

// THEN: Both Web Chats show their messages with their styles, the second one with its send box.
await host.snapshot('local');

// WHEN: The first Web Chat is unmounted.
root1.unmount();

expect(getNumActivities()).toBe(1);

// THEN: The second Web Chat keeps the shared <style> element.
expect(getNumSharedStyleElements()).toBe(1);

// THEN: Only the second Web Chat is shown, with its styles.
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.
74 changes: 74 additions & 0 deletions __tests__/html2/react19/activity.hideShow.fluent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en-US">
<head>
<title>React 19: Web Chat with the Fluent theme keeps its styles when an &lt;Activity&gt; hides and shows it</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 builds of Web Chat and the Fluent theme use `window.React` and `window.ReactDOM`.
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>
<script crossorigin="anonymous" defer src="/__dist__/botframework-webchat-fluent-theme.development.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="module">
import { waitFor } from '@testduet/wait-for';
import { Activity, createElement, version } from 'react';
import { createRoot } from 'react-dom/client';

run(async function () {
const {
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

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

// GIVEN: React 19.2 or later: a hidden <Activity> runs the cleanup of the effects of its children, and keeps them.
expect(version).toBe('19.3.1');

const root = createRoot(document.getElementById('webchat'));
// The same element each time: showing Web Chat again does not re-render it, only its effects run again.
const webChat = createElement(FluentThemeProvider, {}, createElement(ReactWebChat, { directLine, store }));
const render = mode => root.render(createElement(Activity, { mode }, webChat));

// WHEN: Web Chat with the Fluent theme is rendered in a visible <Activity>.
render('visible');

await pageConditions.uiConnected();

// THEN: Web Chat shows the send box of the Fluent theme at the bottom.
await host.snapshot('local');

// WHEN: The <Activity> hides Web Chat.
render('hidden');

await waitFor(() => expect(pageElements.sendBoxTextBox()).toHaveProperty('offsetParent', null));

// WHEN: The <Activity> shows Web Chat again.
render('visible');

await waitFor(() => expect(pageElements.sendBoxTextBox()).not.toHaveProperty('offsetParent', null));

// THEN: Web Chat looks the same as before it was hidden: the send box stays at the bottom.
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.
74 changes: 74 additions & 0 deletions __tests__/html2/react19/activity.hideShow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en-US">
<head>
<title>React 19: Web Chat keeps its styles when an &lt;Activity&gt; hides and shows it</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 { Activity, createElement, 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();

// GIVEN: React 19.2 or later: a hidden <Activity> runs the cleanup of the effects of its children, and keeps them.
expect(version).toBe('19.3.1');

const root = createRoot(document.getElementById('webchat'));
// The same element each time: showing Web Chat again does not re-render it, only its effects run again.
const webChat = createElement(ReactWebChat, { directLine, store });
const render = mode => root.render(createElement(Activity, { mode }, webChat));

// The <style> element of Web Chat's emotion styles, added to `styleOptions.stylesRoot`, which defaults to <head>.
const getNumStyleElements = () => document.head.querySelectorAll('style[data-emotion^="webchat--css-"]').length;

// WHEN: Web Chat is rendered in a visible <Activity>.
render('visible');

await pageConditions.uiConnected();

const numStyleElements = getNumStyleElements();

// THEN: Web Chat shows the send box with its styles.
await host.snapshot('local');

// WHEN: The <Activity> hides Web Chat.
render('hidden');

await waitFor(() => expect(pageElements.sendBoxTextBox()).toHaveProperty('offsetParent', null));

// WHEN: The <Activity> shows Web Chat again.
render('visible');

await waitFor(() => expect(pageElements.sendBoxTextBox()).not.toHaveProperty('offsetParent', null));

// THEN: Web Chat has its <style> element back.
expect(getNumStyleElements()).toBe(numStyleElements);

// THEN: Web Chat looks the same as before it was hidden.
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.
91 changes: 91 additions & 0 deletions __tests__/html2/react19/activity.hideShow.sharedStyles.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!doctype html>
<html lang="en-US">
<head>
<title>React 19: Web Chat shown again by an &lt;Activity&gt; keeps its shared styles when the other unmounts</title>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<style>
#webchat > div {
height: 50%;
}
</style>
</head>
<body>
<main id="webchat">
<div id="webchat-1"></div>
<div id="webchat-2"></div>
</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 { Activity, createElement, 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 };

// The <style> elements of the styles the two Web Chats share.
const getNumSharedStyleElements = () =>
document.head.querySelectorAll('style[data-emotion^="webchat--css-"]').length;

// GIVEN: React 19.2 or later: a hidden <Activity> runs the cleanup of the effects of its children, and keeps them.
expect(version).toBe('19.3.1');

// GIVEN: Two Web Chats with the same nonce and styles root (both default), so they share their styles.
// The second one hides its send box: two unnamed send box landmarks would not be unique.
const root1 = createRoot(document.getElementById('webchat-1'));
const root2 = createRoot(document.getElementById('webchat-2'));
const webChat1 = createElement(ReactWebChat, testHelpers.createDirectLineEmulator());
const webChat2 = testHelpers.createDirectLineEmulator();
const render1 = mode => root1.render(createElement(Activity, { mode }, webChat1));

render1('visible');
root2.render(createElement(ReactWebChat, { ...webChat2, styleOptions: { hideSendBox: true } }));

await pageConditions.uiConnected();

await webChat2.directLine.emulateIncomingActivity('Hello from the second Web Chat.');
await waitFor(() => expect(document.querySelectorAll('.webchat__basic-transcript__activity')).toHaveLength(1));

expect(getNumSharedStyleElements()).toBe(1);

// WHEN: An <Activity> hides the first Web Chat, then shows it again.
render1('hidden');

await waitFor(() => expect(pageElements.sendBoxTextBox()).toHaveProperty('offsetParent', null));

render1('visible');

await waitFor(() => expect(pageElements.sendBoxTextBox()).not.toHaveProperty('offsetParent', null));

// THEN: The first Web Chat shows its send box, the second one its message, both with their styles.
await host.snapshot('local');

// WHEN: The second Web Chat is unmounted.
root2.unmount();

await waitFor(() => expect(document.querySelectorAll('.webchat__basic-transcript__activity')).toHaveLength(0));

// THEN: The first Web Chat keeps the shared <style> element.
expect(getNumSharedStyleElements()).toBe(1);

// THEN: Only the first Web Chat is shown, with its styles.
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
Loading