Skip to content

Commit

Permalink
Add React hooks for customization (part 4) (#2543)
Browse files Browse the repository at this point in the history
* Typo

* Update entry

* Add useActivities, useReferenceGrammarID and useSendBoxDictationStarted

* Add useStyleOptions and useStyleSet

* Fix errors

* Add useStyleOptions and useStyleSet

* Add useLanguage and useLocalize

* Fix ESLint

* Cleanup

* Clean up

* Fix useLocalize

* Revert dev build

* Typo

* Update entry

* Fix ESLint

* Add useStyleOptions and useStyleSet

* Add useStyleOptions and useStyleSet

* Add useLanguage and useLocalize

* Use useLocalize

* Add useStyleOptions and useStyleSet

* Add useLanguage and useLocalize

* Add useAdaptiveCards* and useRenderMarkdownAsHTML

* Update PR number

* Use HostConfig from package

* Update entry

* Fix tests

* Improve test reliability

* Remove useWebSpeechPonyfill test

* Fix tests

* Fix ESLint

* Fix ESLint

* Fix bad merge

* Fix bad merge

* Undo dev changes

* Fix test

* Add test

* Add test

* Update screenshot

* Fix tests
  • Loading branch information
compulim authored Nov 14, 2019
1 parent 39f7c70 commit db2a1bb
Show file tree
Hide file tree
Showing 32 changed files with 389 additions and 197 deletions.
14 changes: 9 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed
### Breaking changes

- `bundle`: Webpack will now use `webpack-stats-plugin` instead of `webpack-visualizer-plugin`, by [@compulim](https://github.com/compulim) in PR [#2584](https://github.com/microsoft/BotFramework-WebChat/pull/2584)
- This will fix [#2583](https://github.com/microsoft/BotFramework-WebChat/issues/2583) by not bringing in transient dependency of React
- To view the bundle stats, browse to https://chrisbateman.github.io/webpack-visualizer/ and drop the file `/packages/bundle/dist/stats.json`
- `adaptiveCardHostConfig` is being renamed to `adaptiveCardsHostConfig`
- If you are using the deprecated `adaptiveCardHostConfig`, we will rename it automatically

### Fixed

Expand All @@ -37,10 +36,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Resolves [#2539](https://github.com/Microsoft/BotFramework-WebChat/issues/2539), added React hooks for customziation, by [@compulim](https://github.com/compulim) and [@corinagum](https://github.com/corinagum), in the following PRs:
- Resolves [#2539](https://github.com/Microsoft/BotFramework-WebChat/issues/2539), added React hooks for customization, by [@compulim](https://github.com/compulim), in the following PRs:
- PR [#2540](https://github.com/microsoft/BotFramework-WebChat/pull/2540): `useActivities`, `useReferenceGrammarID`, `useSendBoxDictationStarted`
- PR [#2541](https://github.com/microsoft/BotFramework-WebChat/pull/2541): `useStyleOptions`, `useStyleSet`
- PR [#2542](https://github.com/microsoft/BotFramework-WebChat/pull/2542): `useLanguage`, `useLocalize`, `useLocalizeDate`
- 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)

### Fixed

Expand Down Expand Up @@ -120,6 +121,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `core-js@3.3.6`
- `component`: Bumps [`adaptivecards@1.2.3`](https://npmjs.com/package/adaptivecards), by [@corinagum](https://github.com/corinagum) in PR [#2523](https://github.com/microsoft/BotFramework-WebChat/pull/2532)
- Bumps Chrome in Docker to 78.0.3904.70, by [@spyip](https://github.com/spyip) in PR [#2545](https://github.com/microsoft/BotFramework-WebChat/pull/2545)
- `bundle`: Webpack will now use `webpack-stats-plugin` instead of `webpack-visualizer-plugin`, by [@compulim](https://github.com/compulim) in PR [#2584](https://github.com/microsoft/BotFramework-WebChat/pull/2584)
- This will fix [#2583](https://github.com/microsoft/BotFramework-WebChat/issues/2583) by not bringing in transient dependency of React
- To view the bundle stats, browse to https://chrisbateman.github.io/webpack-visualizer/ and drop the file `/packages/bundle/dist/stats.json`

### Samples

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions __tests__/adaptiveCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ test('breakfast card with custom host config', async () => {
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('breakfast card with custom style options', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
bubbleTextColor: '#FF0000'
}
}
});

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('card breakfast', { waitForSend: true });

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

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('disable card inputs', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

// 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 return Adaptive Cards host config set in props', async () => {
const { pageObjects } = await setupWebDriver({
props: {
adaptiveCardsHostConfig: {
supportsInteractivity: false
}
}
});

const [adaptiveCardsHostConfig] = await pageObjects.runHook('useAdaptiveCardsHostConfig');

expect(adaptiveCardsHostConfig).toMatchInlineSnapshot(`
Object {
"supportsInteractivity": false,
}
`);
});

test('getter should return default Adaptive Cards host config if not set in props', async () => {
const { pageObjects } = await setupWebDriver();

const [adaptiveCardsHostConfig] = await pageObjects.runHook('useAdaptiveCardsHostConfig');

expect(adaptiveCardsHostConfig.supportsInteractivity).toMatchInlineSnapshot(`true`);
});

test('setter should be undefined', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setAdaptiveCardsHostConfig] = await pageObjects.runHook('useAdaptiveCardsHostConfig');

expect(setAdaptiveCardsHostConfig).toBeUndefined();
});
45 changes: 45 additions & 0 deletions __tests__/hooks/useAdaptiveCardsPackage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { timeouts } from '../constants.json';

// 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 return Adaptive Cards package set in props', async () => {
const { pageObjects } = await setupWebDriver({
props: {
adaptiveCardsPackage: {
__DUMMY__: 0
}
}
});

const [adaptiveCardsPackage] = await pageObjects.runHook('useAdaptiveCardsPackage');

expect(adaptiveCardsPackage).toMatchInlineSnapshot(`
Object {
"__DUMMY__": 0,
}
`);
});

test('getter should return default Adaptive Cards package if not set in props', async () => {
const { pageObjects } = await setupWebDriver();

const [adaptiveCardsPackage] = await pageObjects.runHook('useAdaptiveCardsPackage', [], results =>
results[0].AdaptiveCard.currentVersion.toString()
);

expect(adaptiveCardsPackage).toMatchInlineSnapshot(`"1"`);
});

test('setter should be undefined', async () => {
const { pageObjects } = await setupWebDriver();
const setAdaptiveCardsPackage = await pageObjects.runHook(
'useAdaptiveCardsPackage',
[],
results => typeof results[1] === 'undefined'
);

expect(setAdaptiveCardsPackage).toMatchInlineSnapshot(`true`);
});
26 changes: 26 additions & 0 deletions __tests__/hooks/useRenderMarkdownAsHTML.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { timeouts } from '../constants.json';

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

jest.setTimeout(timeouts.test);

test('renderMarkdown should use Markdown-It if not set in props', async () => {
const { pageObjects } = await setupWebDriver();

await expect(pageObjects.runHook('useRenderMarkdownAsHTML', [], fn => fn('Hello, World!'))).resolves.toBe(
'<p>Hello, World!</p>\n'
);
});

test('renderMarkdown should use custom Markdown transform function from props', async () => {
const { pageObjects } = await setupWebDriver({
props: {
renderMarkdown: text => text.toUpperCase()
}
});

await expect(
pageObjects.runHook('useRenderMarkdownAsHTML', [], fn => fn('Hello, World!'))
).resolves.toMatchInlineSnapshot(`"HELLO, WORLD!"`);
});
3 changes: 3 additions & 0 deletions __tests__/scrollToBottom.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { By } from 'selenium-webdriver';
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 suggestedActionsShown from './setup/conditions/suggestedActionsShown';
import uiConnected from './setup/conditions/uiConnected';
Expand Down Expand Up @@ -48,6 +49,8 @@ test('clicking "New messages" button should scroll to end and stick to bottom',
document.querySelector('[role="log"] > *').scrollTop = 0;
});

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

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

await pageObjects.clickScrollToBottomButton();
Expand Down
5 changes: 5 additions & 0 deletions __tests__/setup/conditions/scrollToBottomButtonVisible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { By, until } from 'selenium-webdriver';

export default function scrollToBottomButtonVisible() {
return until.elementLocated(By.css(`[role="log"] > button:last-child`));
}
2 changes: 1 addition & 1 deletion packages/bundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"clean": "npm run clean:dist && npm run clean:lib",
"clean:dist": "rimraf dist",
"clean:lib": "rimraf lib",
"eslint": "eslint src/**/*.js src/**/*.ts --ignore-pattern *.spec.[jt]sx? --ignore-pattern *.test.[jt]sx?",
"eslint": "eslint src/**/*.js src/**/*.ts --ignore-pattern *.spec.[jt]sx? --ignore-pattern *.test.[jt]sx? --ignore-pattern __tests__",
"prepublishOnly": "npm run build:typecheck && npm run build:babel && webpack-cli",
"watch": "concurrently --names \"babel,typecheck,webpack\" \"npm run watch:babel\" \"npm run watch:typecheck\" \"npm run watch:webpack\"",
"watch:babel": "npm run build:babel-instrumented -- --watch",
Expand Down
105 changes: 58 additions & 47 deletions packages/bundle/src/FullReactWebChat.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,79 @@
import * as adaptiveCards from 'adaptivecards';
import * as defaultAdaptiveCardsPackage from 'adaptivecards';
import BasicWebChat, { concatMiddleware } from 'botframework-webchat-component';
import memoize from 'memoize-one';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useEffect, useMemo } from 'react';

import AdaptiveCardsContext from './adaptiveCards/AdaptiveCardsContext';
import createAdaptiveCardsAttachmentMiddleware from './adaptiveCards/createAdaptiveCardMiddleware';
import createDefaultAdaptiveCardHostConfig from './adaptiveCards/Styles/adaptiveCardHostConfig';
import createStyleSet from './adaptiveCards/Styles/createStyleSetWithAdaptiveCards';
import defaultAdaptiveCardHostConfig from './adaptiveCards/Styles/adaptiveCardHostConfig';
import defaultRenderMarkdown from './renderMarkdown';

// Add additional props to <WebChat>, so it support additional features
class FullReactWebChat extends React.Component {
constructor(props) {
super(props);
const FullReactWebChat = ({
adaptiveCardHostConfig,
adaptiveCardsHostConfig,
adaptiveCardsPackage,
attachmentMiddleware,
renderMarkdown,
styleOptions,
styleSet,
...otherProps
}) => {
useEffect(() => {
adaptiveCardHostConfig &&
console.warn(
'Web Chat: "adaptiveCardHostConfig" is deprecated. Please use "adaptiveCardsHostConfig" instead. "adaptiveCardHostConfig" will be removed on or after 2022-01-01.'
);
}, [adaptiveCardHostConfig]);

this.createAttachmentMiddleware = memoize(
(adaptiveCardHostConfig, middlewareFromProps, styleOptions, renderMarkdown) =>
concatMiddleware(
middlewareFromProps,
createAdaptiveCardsAttachmentMiddleware({
adaptiveCardHostConfig: adaptiveCardHostConfig || defaultAdaptiveCardHostConfig(styleOptions),
adaptiveCards,
renderMarkdown
})
)
);
const patchedStyleSet = useMemo(() => styleSet || createStyleSet(styleOptions), [styleOptions, styleSet]);
const { options: patchedStyleOptions } = patchedStyleSet;

this.memoizeStyleSet = memoize((styleSet, styleOptions) => styleSet || createStyleSet(styleOptions));
this.memoizeRenderMarkdown = memoize((renderMarkdown, { options }) => markdown =>
renderMarkdown(markdown, options)
);
}
const patchedAdaptiveCardsHostConfig = useMemo(
() => adaptiveCardsHostConfig || adaptiveCardHostConfig || createDefaultAdaptiveCardHostConfig(patchedStyleOptions),
[adaptiveCardHostConfig, adaptiveCardsHostConfig, patchedStyleOptions]
);

render() {
const {
adaptiveCardHostConfig,
attachmentMiddleware,
renderMarkdown,
styleOptions,
styleSet,
...otherProps
} = this.props;
const patchedAdaptiveCardsPackage = useMemo(() => adaptiveCardsPackage || defaultAdaptiveCardsPackage, [
adaptiveCardsPackage
]);

const memoizedStyleSet = this.memoizeStyleSet(styleSet, styleOptions);
const memoizedRenderMarkdown =
renderMarkdown || this.memoizeRenderMarkdown(defaultRenderMarkdown, memoizedStyleSet);
const patchedRenderMarkdown = useMemo(
() => renderMarkdown || (markdown => defaultRenderMarkdown(markdown, patchedStyleOptions)),
[renderMarkdown, patchedStyleOptions]
);

return (
const patchedAttachmentMiddleware = useMemo(
() => concatMiddleware(attachmentMiddleware, createAdaptiveCardsAttachmentMiddleware()),
[attachmentMiddleware]
);

const adaptiveCardsContext = useMemo(
() => ({
adaptiveCardsPackage: patchedAdaptiveCardsPackage,
hostConfig: patchedAdaptiveCardsHostConfig
}),
[patchedAdaptiveCardsHostConfig, patchedAdaptiveCardsPackage]
);

return (
<AdaptiveCardsContext.Provider value={adaptiveCardsContext}>
<BasicWebChat
attachmentMiddleware={this.createAttachmentMiddleware(
adaptiveCardHostConfig,
attachmentMiddleware,
memoizedStyleSet.options,
memoizedRenderMarkdown
)}
renderMarkdown={memoizedRenderMarkdown}
attachmentMiddleware={patchedAttachmentMiddleware}
renderMarkdown={patchedRenderMarkdown}
styleOptions={styleOptions}
styleSet={memoizedStyleSet}
styleSet={patchedStyleSet}
{...otherProps}
/>
);
}
}
</AdaptiveCardsContext.Provider>
);
};

FullReactWebChat.defaultProps = {
adaptiveCardHostConfig: undefined,
adaptiveCardsHostConfig: undefined,
adaptiveCardsPackage: undefined,
attachmentMiddleware: undefined,
renderMarkdown: undefined,
styleOptions: undefined,
Expand All @@ -73,6 +82,8 @@ FullReactWebChat.defaultProps = {

FullReactWebChat.propTypes = {
adaptiveCardHostConfig: PropTypes.any,
adaptiveCardsHostConfig: PropTypes.any,
adaptiveCardsPackage: PropTypes.any,
attachmentMiddleware: PropTypes.func,
renderMarkdown: PropTypes.func,
styleOptions: PropTypes.any,
Expand Down
13 changes: 13 additions & 0 deletions packages/bundle/src/__tests__/versionTag.es5.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
process.env.npm_package_version = '0.0.0-test';

describe('loading Web Chat', () => {
test('of variant "es5" should set META tag', () => {
require('../index-es5');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:variant"]').content).toBe('full-es5');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:version"]').content).toBe(
'0.0.0-test'
);
});
});
13 changes: 13 additions & 0 deletions packages/bundle/src/__tests__/versionTag.full.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
process.env.npm_package_version = '0.0.0-test';

describe('loading Web Chat', () => {
test('of variant "full" should set META tag', () => {
require('../index');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:variant"]').content).toBe('full');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:version"]').content).toBe(
'0.0.0-test'
);
});
});
13 changes: 13 additions & 0 deletions packages/bundle/src/__tests__/versionTag.minimal.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
process.env.npm_package_version = '0.0.0-test';

describe('loading Web Chat', () => {
test('of variant "minimal" should set META tag', () => {
require('../index-minimal');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:variant"]').content).toBe('minimal');

expect(document.querySelector('head > meta[name="botframework-webchat:bundle:version"]').content).toBe(
'0.0.0-test'
);
});
});
8 changes: 8 additions & 0 deletions packages/bundle/src/adaptiveCards/AdaptiveCardsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react';

const AdaptiveCardHostConfigContext = createContext({
adaptiveCardsPackage: undefined,
hostConfig: undefined
});

export default AdaptiveCardHostConfigContext;
Loading

0 comments on commit db2a1bb

Please sign in to comment.