Skip to content

[Architecture] Move suggested actions to React context #5490

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

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9fe19b6
Initial commit of state context
compulim May 30, 2025
41b9c0c
Clean up
compulim May 30, 2025
70c19a1
Add stable state hook
compulim May 30, 2025
48fd313
Add useRefWithInit
compulim May 30, 2025
bd523b4
Simplify
compulim May 30, 2025
e08e942
Better ESLint hack
compulim May 30, 2025
c3cd694
Add comment
compulim May 30, 2025
73a92b1
Comment
compulim May 30, 2025
d3ffd68
Use useState instead of useRef for simpler code
compulim May 30, 2025
199012f
Use useMemo
compulim May 30, 2025
fd77bf9
Refactor useStateHook
compulim May 30, 2025
f01d1cc
Add useGetterState
compulim May 30, 2025
f76a4de
Rename
compulim May 30, 2025
cb30700
Clean up
compulim May 30, 2025
dd5a06d
Clean up
compulim May 30, 2025
8ca301f
Styling
compulim May 30, 2025
da91646
Simplify
compulim May 30, 2025
8e93bb2
Use state context version
compulim May 30, 2025
ae508a9
Refactor into react-context package
compulim May 31, 2025
f0f88c3
Rename to useReadonlyState
compulim May 31, 2025
6bc0976
Switch between 2 perf strategy
compulim May 31, 2025
f004838
Add createRawReducer
compulim Jun 2, 2025
55088e6
Use BitContext
compulim Jun 2, 2025
4db3f18
Add deps
compulim Jun 2, 2025
5ef55e9
Move clearSuggestedActionsOnPostActivity saga
compulim Jun 3, 2025
3269282
Add deps
compulim Jun 3, 2025
22fb1a3
Change build order
compulim Jun 3, 2025
d015b98
Use warning instead
compulim Jun 3, 2025
5312d52
Allow arbitrary suffix
compulim Jun 3, 2025
f54f248
Rename
compulim Jun 3, 2025
3ac8e66
Add comment
compulim Jun 3, 2025
c2cfcd7
Rename
compulim Jun 3, 2025
6c1d556
Add valibot
compulim Jun 3, 2025
fd78cdf
Merge branch 'main' into feat-state-context
compulim Jun 3, 2025
0df7fd9
Add ReduxActionSinkComposer
compulim Jun 3, 2025
32f361f
Move sendBoxAttachments from Redux to React
compulim Jun 3, 2025
801f351
Move useSendBoxValue from Redux to React
compulim Jun 3, 2025
9d838e1
Fix sendBoxValue state
compulim Jun 3, 2025
0635125
Fix test
compulim Jun 3, 2025
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
26 changes: 0 additions & 26 deletions __tests__/hooks/useSendBoxValue.js

This file was deleted.

5 changes: 0 additions & 5 deletions __tests__/html/sendAttachmentOn/useSendBoxAttachments.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="text/babel" data-presets="env,stage-3,react">
const {
testHelpers: { createDirectLineEmulator },
WebChat: {
Components: { BasicWebChat, Composer },
hooks: { useSendBoxHooks }
}
} = window;

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

const RunFunction = ({ fn }) => {
fn();

return false;
};

const renderWithFunction = fn =>
new Promise(resolve =>
ReactDOM.render(
<Composer directLine={directLine} sendTypingIndicator={true} store={store}>
<BasicWebChat />
<RunFunction fn={() => resolve(fn && fn())} key={Date.now() + ''} />
</Composer>,
document.getElementById('webchat')
)
);

await renderWithFunction();

await pageConditions.webChatRendered();

// WHEN: At initial.
await pageConditions.uiConnected();

// WHEN: Type "Hello, World!" without send.
await pageObjects.typeInSendBox('Hello, World!');

// THEN: `useActiveTyping` should return "Hello, World!"
await expect(renderWithFunction(() => useSendBoxHooks().useSendBoxValue()[0])).resolves.toBe('Hello, World!');
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="text/babel" data-presets="env,stage-3,react">
const {
testHelpers: { createDirectLineEmulator },
WebChat: {
Components: { BasicWebChat, Composer },
hooks: { useSendBoxHooks },
testIds
}
} = window;

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

const RunFunction = ({ fn }) => {
fn();

return false;
};

const renderWithFunction = fn =>
new Promise(resolve =>
ReactDOM.render(
<Composer directLine={directLine} sendTypingIndicator={true} store={store}>
<BasicWebChat />
<RunFunction fn={() => resolve(fn && fn())} key={Date.now() + ''} />
</Composer>,
document.getElementById('webchat')
)
);

await renderWithFunction();

await pageConditions.webChatRendered();

// WHEN: At initial.
await pageConditions.uiConnected();

// WHEN: `useActiveTyping` to set the text box value.
await renderWithFunction(() => useSendBoxHooks().useSendBoxValue()[1]('Hello, World!'));

// THEN: Send box should be "Hello, World!"
expect(pageElements.byTestId(testIds.sendBoxTextBox)).toHaveProperty('value', 'Hello, World!');
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<script>
location = './useSendBoxAttachments?deprecated';
</script>
</head>
<body></body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<main id="webchat"></main>
<script>
run(async function () {
const useDeprecatedHook = new URL(location).searchParams.has('deprecated');
const directLine = WebChat.createDirectLine({ token: await testHelpers.token.fetchDirectLineToken() });
const store = testHelpers.createStore();

Expand All @@ -23,8 +24,10 @@
await pageConditions.uiConnected();

// WHEN: `useSendBoxAttachments` hook is called to get the attachments.
const initialSendBoxAttachments = await pageObjects.runHook(
({ useSendBoxAttachments }) => useSendBoxAttachments()[0]
const initialSendBoxAttachments = await pageObjects.runHook(hooks =>
useDeprecatedHook
? hooks.useSendBoxAttachments()[0]
: hooks.useSendBoxHooks().useSendBoxAttachments()[0]
);

// THEN: It should return empty array.
Expand Down Expand Up @@ -59,16 +62,25 @@
});

// WHEN: `useSendBoxAttachments` hook is called to set an attachment.
await pageObjects.runHook(({ useSendBoxAttachments }) => {
await pageObjects.runHook(hooks => {
const attachmentsRef = React.useRef([{ blob, thumbnailURL }]);
useSendBoxAttachments()[1](attachmentsRef.current);

if (useDeprecatedHook) {
hooks.useSendBoxAttachments()[1](attachmentsRef.current);
} else {
hooks.useSendBoxHooks().useSendBoxAttachments()[1](attachmentsRef.current);
}
});

// THEN: It should show checkmark on the button and file preview.
await host.snapshot();
await host.snapshot('local');

// WHEN: `useSendBoxAttachments` hook is called to get the attachments.
const sendBoxAttachments = await pageObjects.runHook(({ useSendBoxAttachments }) => useSendBoxAttachments()[0]);
const sendBoxAttachments = await pageObjects.runHook(hooks =>
useDeprecatedHook
? hooks.useSendBoxAttachments()[0]
: hooks.useSendBoxHooks().useSendBoxAttachments()[0]
);

// THEN: It should return 1 attachment.
expect(sendBoxAttachments).toHaveLength(1);
Expand All @@ -80,11 +92,13 @@
// THEN: It should send the attachment (each "run hook" is an activity).
await pageConditions.allOutgoingActivitiesSent();
await pageConditions.numActivitiesShown(5);
await host.snapshot();
await host.snapshot('local');

// WHEN: `useSendBoxAttachments` hook is called to get the attachments.
const finalSendBoxAttachments = await pageObjects.runHook(
({ useSendBoxAttachments }) => useSendBoxAttachments()[0]
const finalSendBoxAttachments = await pageObjects.runHook(hooks =>
useDeprecatedHook
? hooks.useSendBoxAttachments()[0]
: hooks.useSendBoxHooks().useSendBoxAttachments()[0]
);

// THEN: It should return 0 attachments.
Expand Down
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.
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,16 @@
// WHEN: useSuggestedActions() is called with no suggested actions.
renderResult.rerender({ suggestedActions: [] });

// THEN: Should hide suggested actions.
// THEN: UI should hide suggested actions.
await waitFor(() => expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(0));

// THEN: Should return 0 suggested actions.
// THEN: useSuggestedActions() should return no suggested actions.
renderResult.rerender();
await waitFor(() =>
expect(renderResult).toHaveProperty('result.current', [[], expect.any(Function), { activity: undefined }])
);

// THEN: getState() should have 1 suggested action.
// THEN: getState() should have no suggested actions.
expect(store.getState().suggestedActions).toHaveLength(0);
});
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://unpkg.com/@testduet/wait-for@main/dist/wait-for.mjs"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';
import renderHook from '../../hooks/private/renderHook.js';

const {
React: { createElement },
testHelpers: { createDirectLineEmulator },
WebChat: {
Components: { BasicWebChat, Composer },
hooks: { useSuggestedActions },
testIds
}
} = window;

run(async function () {
const { directLine, store } = createDirectLineEmulator();
const WebChatWrapper = ({ children }) =>
createElement(Composer, { directLine, store }, createElement(BasicWebChat), children);

// WHEN: Render initially.
const renderResult = renderHook(
props => {
const state = useSuggestedActions();

if (props) {
state[1](props.suggestedActions);
} else {
return state;
}
},
{
legacyRoot: true,
wrapper: WebChatWrapper
}
);

await pageConditions.uiConnected();

// THEN: useSuggestedActions() getter should return empty array.
await waitFor(() =>
expect(renderResult).toHaveProperty('result.current', [[], expect.anything(), { activity: undefined }])
);

// WHEN: An activity with 2 suggested actions is received.
await directLine.emulateIncomingActivity({
from: { role: 'bot' },
suggestedActions: {
actions: [
{ title: 'Hello, World!', type: 'imBack' },
{ title: 'Aloha!', type: 'imBack' }
],
to: ''
},
text: 'Hello, World!',
type: 'message'
});

// WHEN: A message is being sent.
pageElements.byTestId(testIds.sendBoxTextBox).focus();

await (
await directLine.actPostActivity(async () => {
await host.sendKeys('Good morning!', 'ENTER');
})
).resolveAll();

// THEN: UI should hide suggested actions.
await waitFor(() => expect(pageElements.allByTestId(testIds.suggestedActionButton)).toHaveLength(0));

// THEN: useSuggestedActions() should return no suggested actions.
renderResult.rerender();
await waitFor(() =>
expect(renderResult).toHaveProperty('result.current', [[], expect.any(Function), { activity: undefined }])
);

// THEN: getState() should have no suggested actions.
expect(store.getState().suggestedActions).toHaveLength(0);
});
</script>
</body>
</html>
Loading
Loading