Skip to content

Commit f4699be

Browse files
authored
Ponyfilling window.open (#1704)
* Ponyfilling window.open * Update PR number * Update packages/component/src/Composer.js Co-Authored-By: compulim <[email protected]> * Apply suggestions from code review Co-Authored-By: compulim <[email protected]> * Use production CDN * Prefer cardActionMiddleware in favor of windowOpenPonyfill * Remove test code * Update README.md * Add tests * Update description for cardActionMiddleware * Add missing actions * Remove hideCursor * Blur focus * Wait for outgoing activities
1 parent 28b4e4d commit f4699be

12 files changed

Lines changed: 455 additions & 105 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
3030
- `component`: Allow font family and adaptive cards text color to be set via styleOptions, by [@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n), in PR [#1670](https://github.com/Microsoft/BotFramework-WebChat/pull/1670)
3131
- `component`: Add fallback logic to browser which do not support `window.Intl`, by [@compulim](https://github.com/compulim), in PR [#1696](https://github.com/Microsoft/BotFramework-WebChat/pull/1696)
3232
- `*`: Added `username` back to activity, fixed [#1321](https://github.com/Microsoft/BotFramework-WebChat/issues/1321), by [@compulim](https://github.com/compulim), in PR [#1682](https://github.com/Microsoft/BotFramework-DirectLineJS/pull/1682)
33-
- `component`: Allow root component height & width customization via `styleOptions.rootHeight` and `styleOptions.rootWidth`, by [@tonyanziano](https://github.com/tonyanziano), in PR [#1702](https://github.com/Microsoft/BotFramework-WebChat/pull/1702)
33+
- `component`: Allow root component height and width customization via `styleOptions.rootHeight` and `styleOptions.rootWidth`, by [@tonyanziano](https://github.com/tonyanziano), in PR [#1702](https://github.com/Microsoft/BotFramework-WebChat/pull/1702)
34+
- `component`: Added `cardActionMiddleware` to customize the behavior of card action, by [@compulim](https://github.com/compulim), in PR [#1704](https://github.com/Microsoft/BotFramework-WebChat/pull/1704)
3435

3536
### Changed
3637
- Moved `botAvatarImage` and `userAvatarImage` to `styleOptions.botAvatarImage` and `styleOptions.userAvatarImage` respectively, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486)
@@ -88,6 +89,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
8889
- `component`: [Selectable Activity](https://microsoft.github.io/BotFramework-WebChat/16.customization-selectable-activity/), in [#1624](https://github.com/Microsoft/BotFramework-WebChat/pull/1624)
8990
- `component`: [Chat Send History](https://microsoft.github.io/BotFramework-WebChat/17.chat-send-history/), in [#1678](https://github.com/Microsoft/BotFramework-WebChat/pull/1678)
9091
- `*`: Update `README.md`'s for samples 05-10 [#1444](https://github.com/Microsoft/BotFramework-WebChat/issues/1444) and improve accessibility of anchors [#1681](https://github.com/Microsoft/BotFramework-WebChat/issues/1681), by [@corinagum](https://github.com/corinagum) in PR [#1710](https://github.com/Microsoft/BotFramework-WebChat/pull/1710)
92+
- `component`: [Customizing open URL behavior](https://microsoft.github.io/BotFramework-WebChat/18.customization-open-url), in PR [#1704](https://github.com/Microsoft/BotFramework-WebChat/pull/1704)
9193

9294
## [4.2.0] - 2018-12-11
9395
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ npm run prepublishOnly
242242
| [`15.d.backchannel-send-welcome-event`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.d.backchannel-send-welcome-event) | Advanced tutorial: Demonstrates how to send welcome event with client capabilities such as browser language. | [Welcome Event Demo](https://microsoft.github.io/BotFramework-WebChat/15.d.backchannel-send-welcome-event) |
243243
| [`16.customization-selectable-activity`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/16.customization-selectable-activity) | Advanced tutorial: Demonstrates how to add custom click behavior to each activity. | [Selectable Activity Demo](https://microsoft.github.io/BotFramework-WebChat/16.customization-selectable-activity) |
244244
| [`17.chat-send-history`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/17.chat-send-history) | Advanced tutorial: Demonstrates the ability to save user input and allow the user to step back through previous sent messages. | [Chat Send History Demo](https://microsoft.github.io/BotFramework-WebChat/17.chat-send-history) |
245+
| [`18.customization-open-url`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/18.customization-open-url) | Advanced tutorial: Demonstrates how to customize the open URL behavior. | [Customize Open URL Demo](https://microsoft.github.io/BotFramework-WebChat/18.customization-open-url) |
245246

246247
# Contributions
247248

33.4 KB
Loading
30 KB
Loading

__tests__/cardActionMiddleware.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { By, Key } from 'selenium-webdriver';
2+
3+
import { imageSnapshotOptions, timeouts } from './constants.json';
4+
5+
import allOutgoingActivitiesSent from './setup/conditions/allOutgoingActivitiesSent';
6+
import botConnected from './setup/conditions/botConnected';
7+
import suggestedActionsShowed from './setup/conditions/suggestedActionsShowed';
8+
import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown.js';
9+
10+
// selenium-webdriver API doc:
11+
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
12+
13+
test('card action "openUrl"', async () => {
14+
const { driver, pageObjects } = await setupWebDriver({
15+
props: {
16+
cardActionMiddleware: ({ dispatch }) => next => ({ cardAction }) => {
17+
if (cardAction.type === 'openUrl') {
18+
dispatch({
19+
type: 'WEB_CHAT/SEND_MESSAGE',
20+
payload: {
21+
text: `Navigating to ${ cardAction.value }`
22+
}
23+
});
24+
} else {
25+
return next(cardAction);
26+
}
27+
}
28+
}
29+
});
30+
31+
await driver.wait(botConnected(), timeouts.directLine);
32+
33+
const input = await driver.findElement(By.css('input[type="text"]'));
34+
35+
await input.sendKeys('card-actions', Key.RETURN);
36+
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
37+
await driver.wait(suggestedActionsShowed(), timeouts.directLine);
38+
39+
const openUrlButton = await driver.findElement(By.css('[role="form"] ul > li:first-child button'));
40+
41+
await openUrlButton.click();
42+
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
43+
await driver.wait(minNumActivitiesShown(5), timeouts.directLine);
44+
45+
const base64PNG = await driver.takeScreenshot();
46+
47+
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
48+
}, 60000);
49+
50+
test('card action "signin"', async () => {
51+
const { driver } = await setupWebDriver({
52+
props: {
53+
cardActionMiddleware: ({ dispatch }) => next => ({ cardAction, getSignInUrl }) => {
54+
if (cardAction.type === 'signin') {
55+
getSignInUrl().then(url => {
56+
dispatch({
57+
type: 'WEB_CHAT/SEND_MESSAGE',
58+
payload: {
59+
text: `Signing into ${ new URL(url).host }`
60+
}
61+
});
62+
});
63+
} else {
64+
return next(cardAction);
65+
}
66+
}
67+
}
68+
});
69+
70+
await driver.wait(botConnected(), timeouts.directLine);
71+
72+
const input = await driver.findElement(By.css('input[type="text"]'));
73+
74+
await input.sendKeys('oauth', Key.RETURN);
75+
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
76+
77+
const openUrlButton = await driver.findElement(By.css('[role="log"] ul > li button'));
78+
79+
await openUrlButton.click();
80+
await driver.wait(minNumActivitiesShown(5), timeouts.directLine);
81+
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
82+
83+
// When the "Sign in" button is clicked, the focus move to it, need to blur it.
84+
await driver.executeScript(() => {
85+
for (let element of document.querySelectorAll(':focus')) {
86+
element.blur();
87+
}
88+
});
89+
90+
const base64PNG = await driver.takeScreenshot();
91+
92+
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
93+
}, 60000);

__tests__/setup/setupTestFramework.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ const BROWSER_NAME = process.env.WEBCHAT_TEST_ENV || 'chrome-docker';
1717
// const BROWSER_NAME = 'chrome-docker';
1818
// const BROWSER_NAME = 'chrome-local';
1919

20+
function marshal(props) {
21+
return props && Object.keys(props).reduce((nextProps, key) => {
22+
const { [key]: value } = props;
23+
24+
if (typeof value === 'function') {
25+
nextProps[key] = `() => ${ value.toString() }`;
26+
nextProps.__evalKeys.push(key);
27+
} else {
28+
nextProps[key] = value;
29+
}
30+
31+
return nextProps;
32+
}, {
33+
__evalKeys: []
34+
});
35+
}
36+
2037
expect.extend({
2138
toMatchImageSnapshot: configureToMatchImageSnapshot({
2239
customSnapshotsDir: join(__dirname, '../__image_snapshots__', BROWSER_NAME)
@@ -49,28 +66,18 @@ global.setupWebDriver = async options => {
4966
}
5067

5168
await driver.executeAsyncScript(
52-
(coverage, props, createDirectLineFnString, setupFnString, callback) => {
69+
(coverage, options, callback) => {
5370
window.__coverage__ = coverage;
5471

55-
const setupPromise = setupFnString ? eval(`() => ${ setupFnString }`)()() : Promise.resolve();
56-
57-
setupPromise.then(() => {
58-
main({
59-
createDirectLine: createDirectLineFnString && eval(`() => ${ createDirectLineFnString }`)(),
60-
props
61-
});
62-
63-
callback();
64-
});
72+
main(options).then(() => callback(), callback);
6573
},
6674
global.__coverage__,
67-
options.props,
68-
options.createDirectLine && options.createDirectLine.toString(),
69-
options.setup && options.setup.toString()
75+
marshal({
76+
...options,
77+
props: marshal(options.props)
78+
})
7079
);
7180

72-
await driver.wait(webChatLoaded(), timeouts.navigation);
73-
7481
const pageObjects = createPageObjects(driver);
7582

7683
options.pingBotOnLoad && await pageObjects.pingBot();

__tests__/setup/web/index.html

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,42 +55,54 @@
5555
<script>
5656
window.WebChatTest = { actions: [] };
5757

58-
function main({
59-
createDirectLine,
60-
props
61-
} = {}) {
62-
const webChatScript = document.createElement('script');
58+
function unmarshal({ __evalKeys, ...obj } = {}) {
59+
__evalKeys && __evalKeys.forEach(key => {
60+
obj[key] = eval(obj[key])();
61+
});
62+
63+
return obj;
64+
}
6365

64-
webChatScript.setAttribute('src', '/webchat-instrumented.js');
66+
function loadScript(src) {
67+
return new Promise((resolve, reject) => {
68+
const script = document.createElement('script');
6569

66-
webChatScript.addEventListener('load', async () => {
67-
// In this demo, we are using Direct Line token from MockBot.
68-
// To talk to your bot, you should use the token exchanged using your Direct Line secret.
69-
// You should never put the Direct Line secret in the browser or client app.
70-
// https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication
70+
script.setAttribute('src', '/webchat-instrumented.js');
71+
script.addEventListener('load', resolve);
72+
script.addEventListener('error', ({ error }) => reject(error));
7173

72-
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
73-
const { token } = await res.json();
74+
document.body.appendChild(script);
75+
});
76+
}
7477

75-
const store = window.WebChatTest.store = window.WebChat.createStore({}, () => next => action => {
76-
window.WebChatTest.actions.push(action);
78+
async function main(options) {
79+
let { createDirectLine, props, setup } = unmarshal(options);
7780

78-
return next(action);
79-
});
81+
props = unmarshal(props);
8082

81-
createDirectLine || (createDirectLine = window.WebChat.createDirectLine);
83+
if (setup) { await setup(); }
8284

83-
window.WebChat.renderWebChat({
84-
directLine: createDirectLine({ token }),
85-
store,
86-
username: 'Happy Web Chat user',
87-
...props
88-
}, document.getElementById('webchat'));
85+
await loadScript('/webchat-instrumented.js');
8986

90-
document.querySelector('#webchat > *').focus();
87+
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
88+
const { token } = await res.json();
89+
90+
const store = window.WebChatTest.store = window.WebChat.createStore({}, () => next => action => {
91+
window.WebChatTest.actions.push(action);
92+
93+
return next(action);
9194
});
9295

93-
document.body.appendChild(webChatScript);
96+
createDirectLine || (createDirectLine = window.WebChat.createDirectLine);
97+
98+
window.WebChat.renderWebChat({
99+
directLine: createDirectLine({ token }),
100+
store,
101+
username: 'Happy Web Chat user',
102+
...props
103+
}, document.getElementById('webchat'));
104+
105+
document.querySelector('#webchat > *').focus();
94106
}
95107
</script>
96108
</body>

packages/component/src/Composer.js

Lines changed: 25 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ import {
3333
submitSendBox
3434
} from 'botframework-webchat-core';
3535

36+
import concatMiddleware from './Middleware/concatMiddleware';
3637
import Context from './Context';
38+
import createCoreCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
3739
import createStyleSet from './Styles/createStyleSet';
3840
import defaultAdaptiveCardHostConfig from './Styles/adaptiveCardHostConfig';
3941
import Dictation from './Dictation';
4042
import mapMap from './Utils/mapMap';
43+
import observableToPromise from './Utils/observableToPromise';
4144
import shallowEquals from './Utils/shallowEquals';
4245

4346
// Flywheel object
@@ -66,69 +69,27 @@ function styleSetToClassNames(styleSet) {
6669
return mapMap(styleSet, (style, key) => key === 'options' ? style : css(style));
6770
}
6871

69-
function createCardActionLogic({ directLine, dispatch }) {
72+
function createCardActionLogic({ cardActionMiddleware, directLine, dispatch }) {
73+
const runMiddleware = concatMiddleware(cardActionMiddleware, createCoreCardActionMiddleware())({ dispatch });
74+
7075
return {
71-
onCardAction: (({ displayText, text, type, value }) => {
72-
switch (type) {
73-
case 'imBack':
74-
if (typeof value === 'string') {
75-
// TODO: [P4] Instead of calling dispatch, we should move to dispatchers instead for completeness
76-
dispatch(sendMessage(value, 'imBack'));
77-
} else {
78-
throw new Error('cannot send "imBack" with a non-string value');
79-
}
80-
81-
break;
82-
83-
case 'messageBack':
84-
dispatch(sendMessageBack(value, text, displayText));
85-
86-
break;
87-
88-
case 'postBack':
89-
dispatch(sendPostBack(value));
90-
91-
break;
92-
93-
case 'call':
94-
case 'downloadFile':
95-
case 'openUrl':
96-
case 'playAudio':
97-
case 'playVideo':
98-
case 'showImage':
99-
// TODO: [P3] We should support ponyfill for window.open
100-
// This is as-of v3
101-
window.open(value);
102-
break;
103-
104-
case 'signin':
105-
// TODO: [P3] We should prime the URL into the OAuthCard directly, instead of calling getSessionId on-demand
106-
// This is to eliminate the delay between window.open() and location.href call
107-
108-
const popup = window.open();
109-
110-
if (directLine.getSessionId) {
111-
const subscription = directLine.getSessionId().subscribe(sessionId => {
112-
popup.location.href = `${ value }${ encodeURIComponent(`&code_challenge=${ sessionId }`) }`;
113-
114-
// HACK: Sometimes, the call complete asynchronously and we cannot unsubscribe
115-
// Need to wait some short time here to make sure the subscription variable has setup
116-
setImmediate(() => subscription.unsubscribe());
117-
}, error => {
118-
// TODO: [P3] Let the user know something failed and we cannot proceed
119-
// This is as-of v3 now
120-
console.error(error);
121-
});
122-
} else {
123-
popup.location.href = value;
124-
}
125-
126-
break;
127-
128-
default:
129-
console.error(`Web Chat: received unknown card action "${ type }"`);
130-
break;
131-
}
76+
onCardAction: cardAction => runMiddleware(({ cardAction: { type } }) => {
77+
throw new Error(`Web Chat: received unknown card action "${ type }"`);
78+
})({
79+
cardAction,
80+
getSignInUrl: cardAction.type === 'signin' ? () => {
81+
const { value } = cardAction;
82+
83+
if (directLine.getSessionId) {
84+
// TODO: [P3] We should change this one to async/await.
85+
// This is the first place in this project to use async.
86+
// Thus, we need to add @babel/plugin-transform-runtime and @babel/runtime.
87+
88+
return observableToPromise(directLine.getSessionId()).then(sessionId => `${ value }${ encodeURIComponent(`&code_challenge=${ sessionId }`) }`);
89+
} else {
90+
return value;
91+
}
92+
} : null
13293
})
13394
};
13495
}
@@ -381,9 +342,11 @@ ConnectedComposerWithStore.propTypes = {
381342
activityRenderer: PropTypes.func,
382343
adaptiveCardHostConfig: PropTypes.any,
383344
attachmentRenderer: PropTypes.func,
345+
cardActionMiddleware: PropTypes.func,
384346
groupTimestamp: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
385347
disabled: PropTypes.bool,
386348
grammars: PropTypes.arrayOf(PropTypes.string),
349+
openUrlPonyfillFactory: PropTypes.func,
387350
referenceGrammarID: PropTypes.string,
388351
renderMarkdown: PropTypes.func,
389352
scrollToBottom: PropTypes.func,

0 commit comments

Comments
 (0)