Skip to content
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

Ponyfilling window.open #1704

Merged
merged 14 commits into from
Feb 12, 2019
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `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)
- `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)
- `*`: 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)
- `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)
- `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)
- `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)

### Changed
- Moved `botAvatarImage` and `userAvatarImage` to `styleOptions.botAvatarImage` and `styleOptions.userAvatarImage` respectively, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486)
Expand Down Expand Up @@ -88,6 +89,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `component`: [Selectable Activity](https://microsoft.github.io/BotFramework-WebChat/16.customization-selectable-activity/), in [#1624](https://github.com/Microsoft/BotFramework-WebChat/pull/1624)
- `component`: [Chat Send History](https://microsoft.github.io/BotFramework-WebChat/17.chat-send-history/), in [#1678](https://github.com/Microsoft/BotFramework-WebChat/pull/1678)
- `*`: 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)
- `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)

## [4.2.0] - 2018-12-11
### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ npm run prepublishOnly
| [`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) |
| [`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) |
| [`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) |
| [`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) |

# Contributions

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.
93 changes: 93 additions & 0 deletions __tests__/cardActionMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { By, Key } from 'selenium-webdriver';

import { imageSnapshotOptions, timeouts } from './constants.json';

import allOutgoingActivitiesSent from './setup/conditions/allOutgoingActivitiesSent';
import botConnected from './setup/conditions/botConnected';
import suggestedActionsShowed from './setup/conditions/suggestedActionsShowed';
import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown.js';

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

test('card action "openUrl"', 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(botConnected(), timeouts.directLine);

const input = await driver.findElement(By.css('input[type="text"]'));

await input.sendKeys('card-actions', Key.RETURN);
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
await driver.wait(suggestedActionsShowed(), timeouts.directLine);

const openUrlButton = await driver.findElement(By.css('[role="form"] ul > li:first-child button'));

await openUrlButton.click();
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
await driver.wait(minNumActivitiesShown(5), timeouts.directLine);

const base64PNG = await driver.takeScreenshot();

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

test('card action "signin"', async () => {
const { driver } = await setupWebDriver({
props: {
cardActionMiddleware: ({ dispatch }) => next => ({ cardAction, getSignInUrl }) => {
if (cardAction.type === 'signin') {
getSignInUrl().then(url => {
dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: {
text: `Signing into ${ new URL(url).host }`
}
});
});
} else {
return next(cardAction);
}
}
}
});

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

const input = await driver.findElement(By.css('input[type="text"]'));

await input.sendKeys('oauth', Key.RETURN);
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);

const openUrlButton = await driver.findElement(By.css('[role="log"] ul > li button'));

await openUrlButton.click();
await driver.wait(minNumActivitiesShown(5), timeouts.directLine);
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);

// When the "Sign in" button is clicked, the focus move to it, need to blur it.
await driver.executeScript(() => {
for (let element of document.querySelectorAll(':focus')) {
element.blur();
}
});

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
}, 60000);
39 changes: 23 additions & 16 deletions __tests__/setup/setupTestFramework.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ const BROWSER_NAME = process.env.WEBCHAT_TEST_ENV || 'chrome-docker';
// const BROWSER_NAME = 'chrome-docker';
// const BROWSER_NAME = 'chrome-local';

function marshal(props) {
return props && Object.keys(props).reduce((nextProps, key) => {
const { [key]: value } = props;

if (typeof value === 'function') {
nextProps[key] = `() => ${ value.toString() }`;
nextProps.__evalKeys.push(key);
} else {
nextProps[key] = value;
}

return nextProps;
}, {
__evalKeys: []
});
}

expect.extend({
toMatchImageSnapshot: configureToMatchImageSnapshot({
customSnapshotsDir: join(__dirname, '../__image_snapshots__', BROWSER_NAME)
Expand Down Expand Up @@ -49,28 +66,18 @@ global.setupWebDriver = async options => {
}

await driver.executeAsyncScript(
(coverage, props, createDirectLineFnString, setupFnString, callback) => {
(coverage, options, callback) => {
window.__coverage__ = coverage;

const setupPromise = setupFnString ? eval(`() => ${ setupFnString }`)()() : Promise.resolve();

setupPromise.then(() => {
main({
createDirectLine: createDirectLineFnString && eval(`() => ${ createDirectLineFnString }`)(),
props
});

callback();
});
main(options).then(() => callback(), callback);
},
global.__coverage__,
options.props,
options.createDirectLine && options.createDirectLine.toString(),
options.setup && options.setup.toString()
marshal({
...options,
props: marshal(options.props)
})
);

await driver.wait(webChatLoaded(), timeouts.navigation);

const pageObjects = createPageObjects(driver);

options.pingBotOnLoad && await pageObjects.pingBot();
Expand Down
64 changes: 38 additions & 26 deletions __tests__/setup/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,42 +55,54 @@
<script>
window.WebChatTest = { actions: [] };

function main({
createDirectLine,
props
} = {}) {
const webChatScript = document.createElement('script');
function unmarshal({ __evalKeys, ...obj } = {}) {
__evalKeys && __evalKeys.forEach(key => {
obj[key] = eval(obj[key])();
});

return obj;
}

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

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

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

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

return next(action);
});
props = unmarshal(props);

createDirectLine || (createDirectLine = window.WebChat.createDirectLine);
if (setup) { await setup(); }

window.WebChat.renderWebChat({
directLine: createDirectLine({ token }),
store,
username: 'Happy Web Chat user',
...props
}, document.getElementById('webchat'));
await loadScript('/webchat-instrumented.js');

document.querySelector('#webchat > *').focus();
const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
const { token } = await res.json();

const store = window.WebChatTest.store = window.WebChat.createStore({}, () => next => action => {
window.WebChatTest.actions.push(action);

return next(action);
});

document.body.appendChild(webChatScript);
createDirectLine || (createDirectLine = window.WebChat.createDirectLine);

window.WebChat.renderWebChat({
directLine: createDirectLine({ token }),
store,
username: 'Happy Web Chat user',
...props
}, document.getElementById('webchat'));

document.querySelector('#webchat > *').focus();
}
</script>
</body>
Expand Down
87 changes: 25 additions & 62 deletions packages/component/src/Composer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ import {
submitSendBox
} from 'botframework-webchat-core';

import concatMiddleware from './Middleware/concatMiddleware';
import Context from './Context';
import createCoreCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
import createStyleSet from './Styles/createStyleSet';
import defaultAdaptiveCardHostConfig from './Styles/adaptiveCardHostConfig';
import Dictation from './Dictation';
import mapMap from './Utils/mapMap';
import observableToPromise from './Utils/observableToPromise';
import shallowEquals from './Utils/shallowEquals';

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

function createCardActionLogic({ directLine, dispatch }) {
function createCardActionLogic({ cardActionMiddleware, directLine, dispatch }) {
const runMiddleware = concatMiddleware(cardActionMiddleware, createCoreCardActionMiddleware())({ dispatch });

return {
onCardAction: (({ displayText, text, type, value }) => {
switch (type) {
case 'imBack':
if (typeof value === 'string') {
// TODO: [P4] Instead of calling dispatch, we should move to dispatchers instead for completeness
dispatch(sendMessage(value, 'imBack'));
} else {
throw new Error('cannot send "imBack" with a non-string value');
}

break;

case 'messageBack':
dispatch(sendMessageBack(value, text, displayText));

break;

case 'postBack':
dispatch(sendPostBack(value));

break;

case 'call':
case 'downloadFile':
case 'openUrl':
case 'playAudio':
case 'playVideo':
case 'showImage':
// TODO: [P3] We should support ponyfill for window.open
// This is as-of v3
window.open(value);
break;

case 'signin':
// TODO: [P3] We should prime the URL into the OAuthCard directly, instead of calling getSessionId on-demand
// This is to eliminate the delay between window.open() and location.href call

const popup = window.open();

if (directLine.getSessionId) {
const subscription = directLine.getSessionId().subscribe(sessionId => {
popup.location.href = `${ value }${ encodeURIComponent(`&code_challenge=${ sessionId }`) }`;

// HACK: Sometimes, the call complete asynchronously and we cannot unsubscribe
// Need to wait some short time here to make sure the subscription variable has setup
setImmediate(() => subscription.unsubscribe());
}, error => {
// TODO: [P3] Let the user know something failed and we cannot proceed
// This is as-of v3 now
console.error(error);
});
} else {
popup.location.href = value;
}

break;

default:
console.error(`Web Chat: received unknown card action "${ type }"`);
break;
}
onCardAction: cardAction => runMiddleware(({ cardAction: { type } }) => {
throw new Error(`Web Chat: received unknown card action "${ type }"`);
})({
cardAction,
getSignInUrl: cardAction.type === 'signin' ? () => {
const { value } = cardAction;

if (directLine.getSessionId) {
// TODO: [P3] We should change this one to async/await.
// This is the first place in this project to use async.
// Thus, we need to add @babel/plugin-transform-runtime and @babel/runtime.

return observableToPromise(directLine.getSessionId()).then(sessionId => `${ value }${ encodeURIComponent(`&code_challenge=${ sessionId }`) }`);
} else {
return value;
}
} : null
})
};
}
Expand Down Expand Up @@ -381,9 +342,11 @@ ConnectedComposerWithStore.propTypes = {
activityRenderer: PropTypes.func,
adaptiveCardHostConfig: PropTypes.any,
attachmentRenderer: PropTypes.func,
cardActionMiddleware: PropTypes.func,
groupTimestamp: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
disabled: PropTypes.bool,
grammars: PropTypes.arrayOf(PropTypes.string),
openUrlPonyfillFactory: PropTypes.func,
referenceGrammarID: PropTypes.string,
renderMarkdown: PropTypes.func,
scrollToBottom: PropTypes.func,
Expand Down
Loading