Skip to content

Commit 85d7da8

Browse files
compulimOEvgeny
andauthored
Move suggested actions from Redux store to React hooks (#5489)
* Add suggested actions test * Add react-valibot and redux-store package * Add entry * Sort * Fix aria-keyshortcuts * Add react-valibot deps * Use a single setRawState action * Update comment * Test bidirectional replication * Wait for connected * Clean up * Safe handling actions * No declare global * chore: local lint rules stub * Clean up * Mark as readonly * Use base package * declare var process * Deprecate useSuggestedActions() --------- Co-authored-by: Eugene <EOlonov@gmail.com>
1 parent 43d5b5c commit 85d7da8

File tree

137 files changed

+1432
-259
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+1432
-259
lines changed

.eslintrc.react.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins:
22
- react
33
- react-hooks
4+
- local-rules
45

56
extends:
67
- plugin:react/recommended
@@ -15,6 +16,7 @@ settings:
1516
version: detect
1617

1718
rules:
19+
local-rules/forbid-use-hook-producer: error
1820
react/button-has-type: error
1921
react/default-props-match-prop-types: error
2022
react/destructuring-assignment: error

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
3333
- HTML sanitizer is moved from `renderMarkdown` to HTML content transformer middleware, please refer to PR [#5338](https://github.com/microsoft/BotFramework-WebChat/pull/5338)
3434
- If you customized `renderMarkdown` with a custom HTML sanitizer, please move the HTML sanitizer to the new HTML content transformer middleware
3535
- `useGroupActivities` hook is being deprecated in favor of the `useGroupActivitiesByName` hook. The hook will be removed on or after 2027-05-04
36+
- `useSuggestedActions()` hook is being deprecated in favor of the `useSuggestedActionsHooks().useSuggestedActions()` hook. The hook will be removed on or after 2027-05-30
3637

3738
### Added
3839

@@ -195,6 +196,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
195196
- [`webpack@5.98.0`](https://npmjs.com/package/webpack/)
196197
- Fixed [#5446](https://github.com/microsoft/BotFramework-WebChat/issues/5446). Embedded `uuid` so `microsoft-cognitiveservices-speech-sdk` do not need to use dynamic loading, as this could fail in Webpack 4 environment, in PR [#5445](https://github.com/microsoft/BotFramework-WebChat/pull/5445), by [@compulim](https://github.com/compulim)
197198
- Fixed [#5476](https://github.com/microsoft/BotFramework-WebChat/issues/5476). Modernizing components through memoization and use [`valibot`](https://npmjs.com/package/valibot) for props validation, by [@compulim](https://github.com/compulim)
199+
- Ported `useSuggestedActions` to use React hooks as backend instead of Redux store, in PR [#5489](https://github.com/microsoft/BotFramework-WebChat/pull/5489), by [@compulim](https://github.com/compulim)
198200

199201
### Fixed
200202

@@ -230,6 +232,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
230232
# Removed
231233

232234
- Deprecating `disabled` props and `useDisabled` hook in favor of new `uiState` props and `useUIState` hook, in PR [#5276](https://github.com/microsoft/BotFramework-WebChat/pull/5276), by [@compulim](https://github.com/compulim)
235+
- `useSuggestedActions()` hook is being deprecated in favor of the `useSuggestedActionsHooks().useSuggestedActions()` hook, in PR [#5489](https://github.com/microsoft/BotFramework-WebChat/pull/5489), by [@compulim](https://github.com/compulim)
233236

234237
## [4.18.0] - 2024-07-10
235238

__tests__/html/hooks.useCreateActivityStatusRenderer.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
);
5252

5353
await pageConditions.webChatRendered();
54+
await pageConditions.uiConnected();
5455

5556
await directLine.emulateIncomingActivity({
5657
// Setting sequence ID to simplify the order of these test cases.

__tests__/html2/hooks/private/renderHook.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,7 @@ export default function renderHook(
6767
return { rerender: render, unmount: () => ReactDOM.unmountComponentAtNode(element) };
6868
};
6969

70-
const { rerender: baseRerender, unmount } = render(
71-
React.createElement(TestComponent, { renderCallbackProps: initialProps }),
72-
renderOptions
73-
);
70+
const { rerender: baseRerender, unmount } = render({ renderCallbackProps: initialProps });
7471

7572
function rerender(rerenderCallbackProps) {
7673
return baseRerender({ renderCallbackProps: rerenderCallbackProps });
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const {
14+
testHelpers: { createDirectLineEmulator }
15+
} = window;
16+
17+
const { directLine, store } = createDirectLineEmulator();
18+
19+
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));
20+
21+
await pageConditions.uiConnected();
22+
});
23+
</script>
24+
</body>
25+
</html>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.development.js"></script>
6+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.development.js"></script>
7+
<script crossorigin="anonymous" src="/test-harness.js"></script>
8+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
9+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
10+
</head>
11+
<body>
12+
<main id="webchat"></main>
13+
<script type="importmap">
14+
{
15+
"imports": {
16+
"@testduet/wait-for": "https://unpkg.com/@testduet/wait-for@main/dist/wait-for.mjs"
17+
}
18+
}
19+
</script>
20+
<script type="module">
21+
import { waitFor } from '@testduet/wait-for';
22+
import renderHook from '../../hooks/private/renderHook.js';
23+
24+
const {
25+
React: { createElement },
26+
testHelpers: { createDirectLineEmulator },
27+
WebChat: {
28+
Components: { BasicWebChat, Composer },
29+
hooks: { useSuggestedActions },
30+
testIds
31+
}
32+
} = window;
33+
34+
run(async function () {
35+
const { directLine, store } = createDirectLineEmulator();
36+
const WebChatWrapper = ({ children }) =>
37+
createElement(Composer, { directLine, store }, createElement(BasicWebChat), children);
38+
39+
// WHEN: Render initially.
40+
const renderResult = renderHook(
41+
props => {
42+
const state = useSuggestedActions();
43+
44+
if (props) {
45+
state[1](props.suggestedActions);
46+
} else {
47+
return state;
48+
}
49+
},
50+
{
51+
legacyRoot: true,
52+
wrapper: WebChatWrapper
53+
}
54+
);
55+
56+
await pageConditions.uiConnected();
57+
58+
// THEN: useSuggestedActions() getter should return empty array.
59+
await waitFor(() =>
60+
expect(renderResult).toHaveProperty('result.current', [[], expect.anything(), { activity: undefined }])
61+
);
62+
63+
// WHEN: An activity with 2 suggested actions is received.
64+
await directLine.emulateIncomingActivity({
65+
from: { role: 'bot' },
66+
suggestedActions: {
67+
actions: [
68+
{ title: 'Hello, World!', type: 'imBack' },
69+
{ title: 'Aloha!', type: 'imBack' }
70+
],
71+
to: ''
72+
},
73+
text: 'Hello, World!',
74+
type: 'message'
75+
});
76+
77+
// THEN: useSuggestedActions() getter should return 2 suggested actions and origin activity.
78+
renderResult.rerender();
79+
80+
await waitFor(() =>
81+
expect(renderResult).toHaveProperty('result.current', [
82+
[
83+
{ title: 'Hello, World!', type: 'imBack' },
84+
{ title: 'Aloha!', type: 'imBack' }
85+
],
86+
expect.any(Function),
87+
{
88+
activity: expect.objectContaining({
89+
from: { role: 'bot' },
90+
suggestedActions: {
91+
actions: [
92+
{ title: 'Hello, World!', type: 'imBack' },
93+
{ title: 'Aloha!', type: 'imBack' }
94+
],
95+
to: ''
96+
},
97+
text: 'Hello, World!',
98+
type: 'message'
99+
})
100+
}
101+
])
102+
);
103+
104+
// WHEN: useSuggestedActions() setter is called with 1 suggested action.
105+
renderResult.rerender({ suggestedActions: [{ title: 'Good morning!', type: 'imBack' }] });
106+
107+
// THEN: Should show 1 suggested action.
108+
await waitFor(() => expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(1));
109+
expect(pageElements.allByTestId(testIds.suggestedActionButton)[0]).toHaveProperty(
110+
'textContent',
111+
'Good morning!'
112+
);
113+
114+
// THEN: Should return 1 suggested action.
115+
renderResult.rerender();
116+
await waitFor(() =>
117+
expect(renderResult).toHaveProperty('result.current', [
118+
[{ title: 'Good morning!', type: 'imBack' }],
119+
expect.any(Function),
120+
{ activity: undefined }
121+
])
122+
);
123+
124+
// THEN: getState() should have 1 suggested action.
125+
expect(store.getState().suggestedActions).toHaveLength(1);
126+
expect(store.getState().suggestedActions[0]).toEqual({ title: 'Good morning!', type: 'imBack' });
127+
128+
// WHEN: useSuggestedActions() is called with no suggested actions.
129+
renderResult.rerender({ suggestedActions: [] });
130+
131+
// THEN: Should hide suggested actions.
132+
await waitFor(() => expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(0));
133+
134+
// THEN: Should return 0 suggested actions.
135+
renderResult.rerender();
136+
await waitFor(() =>
137+
expect(renderResult).toHaveProperty('result.current', [[], expect.any(Function), { activity: undefined }])
138+
);
139+
140+
// THEN: getState() should have 1 suggested action.
141+
expect(store.getState().suggestedActions).toHaveLength(0);
142+
});
143+
</script>
144+
</body>
145+
</html>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.development.js"></script>
6+
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.development.js"></script>
7+
<script crossorigin="anonymous" src="/test-harness.js"></script>
8+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
9+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
10+
</head>
11+
<body>
12+
<main id="webchat"></main>
13+
<script type="importmap">
14+
{
15+
"imports": {
16+
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
17+
}
18+
}
19+
</script>
20+
<script type="module">
21+
import { waitFor } from '@testduet/wait-for';
22+
import renderHook from '../../hooks/private/renderHook.js';
23+
24+
run(async function () {
25+
const {
26+
React: { createElement },
27+
testHelpers: { createDirectLineEmulator },
28+
WebChat: {
29+
Components: { BasicWebChat, Composer },
30+
hooks: { useSuggestedActions },
31+
testIds
32+
}
33+
} = window;
34+
35+
const { directLine, store } = createDirectLineEmulator();
36+
37+
// WHEN: Render initially.
38+
const renderResult = renderHook(() => useSuggestedActions(), {
39+
legacyRoot: true,
40+
wrapper: ({ children }) =>
41+
createElement(Composer, { directLine, store }, createElement(BasicWebChat), children)
42+
});
43+
44+
await pageConditions.uiConnected();
45+
46+
// WHEN: Receive an activity with a suggested action.
47+
await directLine.emulateIncomingActivity({
48+
from: { role: 'bot' },
49+
suggestedActions: {
50+
actions: [{ title: 'Aloha!', type: 'imBack' }],
51+
to: ''
52+
},
53+
text: 'Hello, World!',
54+
type: 'message'
55+
});
56+
57+
await pageConditions.numActivitiesShown(1);
58+
59+
// THEN: Should have one suggested action button shown.
60+
expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(1);
61+
expect(pageElements.allByTestId(testIds.suggestedActionButton)[0]).toHaveProperty('textContent', 'Aloha!');
62+
63+
// THEN: getState() should have 1 suggested actions and origin activity.
64+
expect(store.getState().suggestedActions).toHaveLength(1);
65+
expect(store.getState().suggestedActions[0]).toEqual({ title: 'Aloha!', type: 'imBack' });
66+
expect(store.getState().suggestedActionsOriginActivity).toEqual({
67+
activity: expect.objectContaining({
68+
from: { role: 'bot' },
69+
suggestedActions: {
70+
actions: [{ title: 'Aloha!', type: 'imBack' }],
71+
to: ''
72+
},
73+
text: 'Hello, World!',
74+
type: 'message'
75+
})
76+
});
77+
78+
// WHEN: Dispatching "WEB_CHAT/CLEAR_SUGGESTED_ACTIONS" action.
79+
store.dispatch({ type: 'WEB_CHAT/CLEAR_SUGGESTED_ACTIONS' });
80+
81+
// THEN: Should not remove activity.
82+
expect(pageElements.activities()).toHaveLength(1);
83+
84+
// THEN: Should have cleared suggested action.
85+
await waitFor(() => expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(0));
86+
87+
// THEN: useSuggestedActions() should have emptied.
88+
renderResult.rerender();
89+
expect(renderResult).toHaveProperty('result.current', [[], expect.any(Function), { activity: undefined }]);
90+
});
91+
</script>
92+
</body>
93+
</html>

0 commit comments

Comments
 (0)