Skip to content

Commit

Permalink
Add send box related hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
compulim committed Nov 4, 2019
1 parent 03d2b31 commit b09d0c4
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- PR [#2549](https://github.com/microsoft/BotFramework-WebChat/pull/2549): `useSuggestedActions`
- PR [#2550](https://github.com/microsoft/BotFramework-WebChat/pull/2550): `useConnectivityStatus`, `useGroupTimestamp`, `useTimeoutForSend`, `useUserID`, `useUsername`
- PR [#2551](https://github.com/microsoft/BotFramework-WebChat/pull/2551): `useLastTypingAt`, `useSendTypingIndicator`, `useTypingIndicator`
- PR [#2552](https://github.com/microsoft/BotFramework-WebChat/pull/2552): `useFocusSendBox`, `useScrollToEnd`, `useSendBoxValue`, `useSubmitSendBox`, `useTextBoxSubmit`, `useTextBoxValue`
- Bring your own Adaptive Cards package by specifying `adaptiveCardsPackage` prop, by [@compulim](https://github.com/compulim) in PR [#2543](https://github.com/microsoft/BotFramework-WebChat/pull/2543)

### Fixed
Expand Down
34 changes: 34 additions & 0 deletions __tests__/hooks/useScrollToEnd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { imageSnapshotOptions, timeouts } from '../constants.json';

import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
import scrollToBottomButtonVisible from '../setup/conditions/scrollToBottomButtonVisible';
import scrollToBottomCompleted from '../setup/conditions/scrollToBottomCompleted';
import uiConnected from '../setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('calling scrollToEnd should scroll to end', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.sendMessageViaSendBox('help');
await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

await driver.executeScript(() => {
document.querySelector('[role="log"] > *').scrollTop = 0;
});

await driver.wait(scrollToBottomButtonVisible(), timeouts.ui);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.runHook('useScrollToEnd', [], scrollToEnd => scrollToEnd());
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});
60 changes: 60 additions & 0 deletions __tests__/hooks/useSendBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { timeouts } from '../constants.json';

import speechRecognitionStartCalled from '../setup/conditions/speechRecognitionStartCalled';
import uiConnected from '../setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('sendBoxDictationStarted should return if dictation is started or not', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
webSpeechPonyfillFactory: () => window.WebSpeechMock
}
});

await driver.wait(uiConnected(), timeouts.directLine);

await expect(
pageObjects.runHook('useSendBoxDictationStarted', [], sendBoxDictationStarted => sendBoxDictationStarted[0])
).resolves.toMatchInlineSnapshot(`false`);

await pageObjects.clickMicrophoneButton();

await driver.wait(speechRecognitionStartCalled(), timeouts.ui);

await expect(
pageObjects.runHook('useSendBoxDictationStarted', [], sendBoxDictationStarted => sendBoxDictationStarted[0])
).resolves.toMatchInlineSnapshot(`true`);

await pageObjects.putSpeechRecognitionResult('recognizing', 'Hello');

await expect(
pageObjects.runHook('useSendBoxDictationStarted', [], sendBoxDictationStarted => sendBoxDictationStarted[0])
).resolves.toMatchInlineSnapshot(`true`);
});

test('sendBoxDictationStarted should return false when synthesizing', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
webSpeechPonyfillFactory: () => window.WebSpeechMock
}
});

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaMicrophone('Hello, World!');
await expect(pageObjects.startSpeechSynthesize());
await pageObjects.clickMicrophoneButton();

await expect(
pageObjects.runHook('useSendBoxDictationStarted', [], sendBoxDictationStarted => sendBoxDictationStarted[0])
).resolves.toMatchInlineSnapshot(`false`);

await pageObjects.endSpeechSynthesize();

await expect(
pageObjects.runHook('useSendBoxDictationStarted', [], sendBoxDictationStarted => sendBoxDictationStarted[0])
).resolves.toMatchInlineSnapshot(`true`);
});
26 changes: 26 additions & 0 deletions __tests__/hooks/useSendBoxValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { timeouts } from '../constants.json';

import uiConnected from '../setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('getter should get the send box text', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.typeOnSendBox('Hello, World!');
await expect(pageObjects.runHook('useSendBoxValue', [], result => result[0])).resolves.toBe('Hello, World!');
});

test('setter should set the send box text', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.runHook('useSendBoxValue', [], result => result[1]('Hello, World!'));
await expect(pageObjects.getSendBoxText()).resolves.toBe('Hello, World!');
});
24 changes: 24 additions & 0 deletions __tests__/hooks/useSubmitSendBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { imageSnapshotOptions, timeouts } from '../constants.json';

import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
import uiConnected from '../setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('calling submitSendBox should send the message in send box', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.typeOnSendBox('Hello, World!');
await pageObjects.runHook('useSubmitSendBox', [], submitSendBox => submitSendBox());

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
38 changes: 38 additions & 0 deletions __tests__/hooks/useTextBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { imageSnapshotOptions, timeouts } from '../constants.json';

import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
import scrollToBottomCompleted from '../setup/conditions/scrollToBottomCompleted';
import uiConnected from '../setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('calling submit should scroll to end', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.typeOnSendBox('help');

await expect(pageObjects.runHook('useTextBoxValue', [], textBoxValue => textBoxValue[0])).resolves.toBe('help');

await pageObjects.clickSendButton();

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

await driver.executeScript(() => {
document.querySelector('[role="log"] > *').scrollTop = 0;
});

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.runHook('useTextBoxValue', [], textBoxValue => textBoxValue[1]('Hello, World!'));
await pageObjects.runHook('useTextBoxSubmit', [], textBoxSubmit => textBoxSubmit());

await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});
15 changes: 5 additions & 10 deletions packages/component/src/Activity/ScrollToEndButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

import connectToWebChat from '../connectToWebChat';
import Localize from '../Localization/Localize';
import useScrollToEnd from '../hooks/useScrollToEnd';
import useStyleSet from '../hooks/useStyleSet';

const ScrollToEndButton = ({ className, scrollToEnd }) => {
const ScrollToEndButton = ({ className }) => {
const [{ scrollToEndButton: scrollToEndButtonStyleSet }] = useStyleSet();
const scrollToEnd = useScrollToEnd();

return (
<button className={classNames(scrollToEndButtonStyleSet + '', className + '')} onClick={scrollToEnd} type="button">
Expand All @@ -23,18 +24,12 @@ ScrollToEndButton.defaultProps = {
};

ScrollToEndButton.propTypes = {
className: PropTypes.string,
scrollToEnd: PropTypes.func.isRequired,
styleSet: PropTypes.shape({
scrollToEndButton: PropTypes.any.isRequired
}).isRequired
className: PropTypes.string
};

const WebChatConnectedScrollToEndButton = connectToWebChat(({ scrollToEnd }) => ({ scrollToEnd }))(ScrollToEndButton);

const ConnectedScrollToEndButton = props => (
<ScrollToBottomStateContext.Consumer>
{({ sticky }) => !sticky && <WebChatConnectedScrollToEndButton {...props} />}
{({ sticky }) => !sticky && <ScrollToEndButton {...props} />}
</ScrollToBottomStateContext.Consumer>
);

Expand Down
31 changes: 24 additions & 7 deletions packages/component/src/Activity/SendStatus.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Constants } from 'botframework-webchat-core';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback } from 'react';

import connectToWebChat from '../connectToWebChat';
import ScreenReaderText from '../ScreenReaderText';
import useFocusSendBox from '../hooks/useFocusSendBox';
import useLocalize from '../hooks/useLocalize';
import usePostActivity from '../hooks/usePostActivity';
import useStyleSet from '../hooks/useStyleSet';

const {
Expand All @@ -28,8 +30,10 @@ const connectSendStatus = (...selectors) =>
...selectors
);

const SendStatus = ({ activity: { channelData: { state } = {} }, retrySend }) => {
const SendStatus = ({ activity }) => {
const [{ sendStatus: sendStatusStyleSet }] = useStyleSet();
const focusSendBox = useFocusSendBox();
const postActivity = usePostActivity();

// TODO: [P4] Currently, this is the only place which use a templated string
// We could refactor this into a general component if there are more templated strings
Expand All @@ -38,7 +42,21 @@ const SendStatus = ({ activity: { channelData: { state } = {} }, retrySend }) =>
const retryText = useLocalize('Retry');
const sendFailedText = useLocalize('SEND_FAILED_KEY');

const handleRetryClick = useCallback(
evt => {
evt.preventDefault();

postActivity(activity);

// After clicking on "retry", the button will be gone and focus will be lost (back to document.body)
// We want to make sure the user stay inside Web Chat
focusSendBox();
},
[activity, focusSendBox, postActivity]
);

const sendFailedRetryMatch = /\{Retry\}/u.exec(sendFailedText);
const { channelData: { state } = {} } = status;

return (
<React.Fragment>
Expand All @@ -50,13 +68,13 @@ const SendStatus = ({ activity: { channelData: { state } = {} }, retrySend }) =>
sendFailedRetryMatch ? (
<React.Fragment>
{sendFailedText.substr(0, sendFailedRetryMatch.index)}
<button onClick={retrySend} type="button">
<button onClick={handleRetryClick} type="button">
{retryText}
</button>
{sendFailedText.substr(sendFailedRetryMatch.index + sendFailedRetryMatch[0].length)}
</React.Fragment>
) : (
<button onClick={retrySend} type="button">
<button onClick={handleRetryClick} type="button">
{sendFailedText}
</button>
)
Expand All @@ -73,10 +91,9 @@ SendStatus.propTypes = {
channelData: PropTypes.shape({
state: PropTypes.string
})
}).isRequired,
retrySend: PropTypes.func.isRequired
}).isRequired
};

export default connectSendStatus()(SendStatus);
export default SendStatus;

export { connectSendStatus };
12 changes: 4 additions & 8 deletions packages/component/src/Dictation.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import useActivities from './hooks/useActivities';
import useDisabled from './hooks/useDisabled';
import useEmitTypingIndicator from './hooks/useEmitTypingIndicator';
import useLanguage from './hooks/useLanguage';
import useSendBoxValue from './hooks/useSendBoxValue';
import useSendTypingIndicator from './hooks/useSendTypingIndicator';
import useSubmitSendBox from './hooks/useSubmitSendBox';

const {
DictateState: { DICTATING, IDLE, STARTING }
Expand All @@ -20,17 +22,17 @@ const Dictation = ({
onError,
setDictateInterims,
setDictateState,
setSendBox,
startSpeakingActivity,
stopDictate,
submitSendBox,
webSpeechPonyfill: { SpeechGrammarList, SpeechRecognition } = {}
}) => {
const [, setSendBox] = useSendBoxValue();
const [activities] = useActivities();
const [disabled] = useDisabled();
const [language] = useLanguage();
const [sendTypingIndicator] = useSendTypingIndicator();
const emitTypingIndicator = useEmitTypingIndicator();
const submitSendBox = useSubmitSendBox();

const numSpeakingActivities = useMemo(() => activities.filter(({ channelData: { speak } = {} }) => speak).length, [
activities
Expand Down Expand Up @@ -96,10 +98,8 @@ Dictation.propTypes = {
onError: PropTypes.func,
setDictateInterims: PropTypes.func.isRequired,
setDictateState: PropTypes.func.isRequired,
setSendBox: PropTypes.func.isRequired,
startSpeakingActivity: PropTypes.func.isRequired,
stopDictate: PropTypes.func.isRequired,
submitSendBox: PropTypes.func.isRequired,
webSpeechPonyfill: PropTypes.shape({
SpeechGrammarList: PropTypes.any.isRequired,
SpeechRecognition: PropTypes.any.isRequired
Expand All @@ -112,20 +112,16 @@ export default connectToWebChat(
postActivity,
setDictateInterims,
setDictateState,
setSendBox,
startSpeakingActivity,
stopDictate,
submitSendBox,
webSpeechPonyfill
}) => ({
dictateState,
postActivity,
setDictateInterims,
setDictateState,
setSendBox,
startSpeakingActivity,
stopDictate,
submitSendBox,
webSpeechPonyfill
})
)(Dictation);
Loading

0 comments on commit b09d0c4

Please sign in to comment.