Skip to content

Commit

Permalink
Add React Hooks for customization (part 6) (#2547)
Browse files Browse the repository at this point in the history
* Add multiple post activity related hooks

* Fix suggested action

* Fix tests

* Fix ESLint

* Fix ESLint
  • Loading branch information
compulim authored Nov 14, 2019
1 parent 233d30d commit c577bf7
Show file tree
Hide file tree
Showing 33 changed files with 473 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- PR [#2543](https://github.com/microsoft/BotFramework-WebChat/pull/2543): `useAdaptiveCardsHostConfig`, `useAdaptiveCardsPackage`, `useRenderMarkdownAsHTML`
- 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)
- PR [#2544](https://github.com/microsoft/BotFramework-WebChat/pull/2544): `useAvatarForBot`, `useAvatarForUser`
- PR [#2547](https://github.com/microsoft/BotFramework-WebChat/pull/2547): `useEmitTypingIndicator`, `usePeformCardAction`, `usePostActivity`, `useSendEvent`, `useSendFiles`, `useSendMessage`, `useSendMessageBack`, `useSendPostBack`
- Fixes [#2597](https://github.com/microsoft/BotFramework-WebChat/issues/2597). Modify `watch` script to `start` and add `tableflip` script for throwing `node_modules`, by [@corinagum](https://github.com/corinagum) in PR [#2598](https://github.com/microsoft/BotFramework-WebChat/pull/2598)

### Fixed
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.
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
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.
23 changes: 23 additions & 0 deletions __tests__/hooks/useEmitTypingIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { timeouts } from '../constants.json';

import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
import typingActivityReceived from '../setup/conditions/typingActivityReceived';
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 emitTypingIndicator should send a typing activity', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true });

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

await pageObjects.runHook('useEmitTypingIndicator', [], fn => fn());

await driver.wait(typingActivityReceived(), timeouts.directLine);
});
36 changes: 36 additions & 0 deletions __tests__/hooks/usePerformCardAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { imageSnapshotOptions, 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('calling performCardAction should send card action to middleware', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
cardActionMiddleware: ({ dispatch }) => next => ({ cardAction }) => {
if (cardAction.type === 'openUrl') {
dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: `Navigating to ${cardAction.value}`
}
});
} else {
return next(cardAction);
}
}
}
});

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.runHook('usePerformCardAction', [], performCardAction =>
performCardAction({ type: 'openUrl', value: 'about:blank' })
);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
28 changes: 28 additions & 0 deletions __tests__/hooks/usePostActivity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 postActivity should send an activity', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.runHook('usePostActivity', [], fn =>
fn({
text: 'Hello, World!',
type: 'message'
})
);

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
31 changes: 31 additions & 0 deletions __tests__/hooks/useSendFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 sendFile should send files', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.runHook('useSendFiles', [], sendFiles => {
const blob1 = new Blob([new ArrayBuffer(1024)]);
const blob2 = new Blob([new ArrayBuffer(1024)]);

blob1.name = 'index.png';
blob2.name = 'index2.png';

sendFiles([blob1, blob2]);
});

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
23 changes: 23 additions & 0 deletions __tests__/hooks/useSendMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 sendMessage should send a message activity', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.runHook('useSendMessage', [], sendMessage => sendMessage('Hello, World!'));

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
25 changes: 25 additions & 0 deletions __tests__/hooks/useSendMessageBack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 sendMessageBack should send a message back activity', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.runHook('useSendMessageBack', [], sendMessageBack =>
sendMessageBack({ hello: 'World!' }, 'Aloha!', 'Display text')
);

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
23 changes: 23 additions & 0 deletions __tests__/hooks/useSendPostBack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 sendPostBack should send a post back activity', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.runHook('useSendPostBack', [], sendPostBack => sendPostBack({ hello: 'World!' }));

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,10 @@ const AdaptiveCardAttachment = ({ attachment: { content } }) => {
// TODO: [P3] Move from "onParseError" to "card.parse(json, errors)"
AdaptiveCard.onParseError = error => errors.push(error);

if (typeof content !== 'object') {
content = {};
}

card.parse(
stripSubmitAction({
version: '1.0',
...content
...(typeof content === 'object' ? content : {})
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useAdaptiveCardsHostConfig from '../hooks/useAdaptiveCardsHostConfig';
import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage';

const { ErrorBox } = Components;
const { useLocalize, useRenderMarkdownAsHTML, useStyleSet } = hooks;
const { useLocalize, usePerformCardAction, useRenderMarkdownAsHTML, useStyleSet } = hooks;

function isPlainObject(obj) {
return Object.getPrototypeOf(obj) === Object.prototype;
Expand Down Expand Up @@ -64,11 +64,12 @@ function saveInputValues(element) {
});
}

const AdaptiveCardRenderer = ({ adaptiveCard, disabled, performCardAction, tapAction }) => {
const AdaptiveCardRenderer = ({ adaptiveCard, disabled, tapAction }) => {
const [{ adaptiveCardRenderer: adaptiveCardRendererStyleSet }] = useStyleSet();
const [{ HostConfig }] = useAdaptiveCardsPackage();
const [adaptiveCardsHostConfig] = useAdaptiveCardsHostConfig();
const errorMessage = useLocalize('Adaptive Card render error');
const performCardAction = usePerformCardAction();
const renderMarkdownAsHTML = useRenderMarkdownAsHTML();

const [error, setError] = useState();
Expand Down Expand Up @@ -215,7 +216,6 @@ const AdaptiveCardRenderer = ({ adaptiveCard, disabled, performCardAction, tapAc
AdaptiveCardRenderer.propTypes = {
adaptiveCard: PropTypes.any.isRequired,
disabled: PropTypes.bool,
performCardAction: PropTypes.func.isRequired,
tapAction: PropTypes.shape({
type: PropTypes.string.isRequired,
value: PropTypes.string
Expand All @@ -227,8 +227,7 @@ AdaptiveCardRenderer.defaultProps = {
tapAction: undefined
};

export default connectToWebChat(({ disabled, onCardAction, tapAction }) => ({
export default connectToWebChat(({ disabled, tapAction }) => ({
disabled,
performCardAction: onCardAction,
tapAction
}))(AdaptiveCardRenderer);
30 changes: 24 additions & 6 deletions packages/component/src/Activity/SendStatus.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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 useLocalize from '../hooks/useLocalize';
import usePostActivity from '../hooks/usePostActivity';
import useStyleSet from '../hooks/useStyleSet';

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

const SendStatus = ({ activity: { channelData: { state } = {} }, retrySend }) => {
const SendStatus = ({ activity, focusSendBox }) => {
const [{ sendStatus: sendStatusStyleSet }] = useStyleSet();
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 +40,23 @@ 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 }
} = activity;

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 @@ -74,9 +92,9 @@ SendStatus.propTypes = {
state: PropTypes.string
})
}).isRequired,
retrySend: PropTypes.func.isRequired
focusSendBox: PropTypes.func.isRequired
};

export default connectSendStatus()(SendStatus);
export default connectSendStatus(({ focusSendBox }) => ({ focusSendBox }))(SendStatus);

export { connectSendStatus };
Loading

0 comments on commit c577bf7

Please sign in to comment.