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

Make thumbnails for uploading pictures #2206

Merged
merged 28 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Added bubble nub and style options, by [@compulim](https://github.com/compulim), in PR [#2137](https://github.com/Microsoft/BotFramework-WebChat/pull/2137)
- Fix [#1808](https://github.com/microsoft/BotFramework-WebChat/issues/1808). Added documentation on activity types, by [@corinagum](https://github.com/corinagum) in PR [#2228](https://github.com/microsoft/BotFramework-WebChat/pull/2228)
- Make thumbnails when uploading GIF/JPEG/PNG and store it in `channelData.attachmentThumbnails`, by [@compulim](https://github.com/compulim), in PR [#2206](https://github.com/microsoft/BotFramework-WebChat/pull/2206), requires modern browsers with following features:
- [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
- [`createImageBitmap`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap)
- [`MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel)/[`MessagePort`](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort)
- [`OffscreenCanvas`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas)
- Specifically [`OffscreenCanvas.getContext('2d')`](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/getContext)

### Samples

Expand Down
6 changes: 6 additions & 0 deletions Dockerfile-selenium
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# https://github.com/SeleniumHQ/docker-selenium
# https://hub.docker.com/r/selenium/standalone-chrome/tags/

FROM selenium/standalone-chrome:3.141.59-radium

ADD __tests__/setup/local ~/Downloads
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.
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.
Binary file added __tests__/setup/local/empty.mp3
Binary file not shown.
Binary file added __tests__/setup/local/empty.pdf
Binary file not shown.
Binary file added __tests__/setup/local/empty.zip
Binary file not shown.
Binary file added __tests__/setup/local/seaofthieves.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions __tests__/setup/pageObjects/getActivityElements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { By } from 'selenium-webdriver';

export default async function getActivityElements(driver) {
return await driver.findElements(By.css(`[role="listitem"]`));
}
2 changes: 1 addition & 1 deletion __tests__/setup/pageObjects/getSendBoxTextBox.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { By } from 'selenium-webdriver';

export default async function isRecognizingSpeech(driver) {
export default async function getSendBoxTextBox(driver) {
return await driver.findElement(By.css('[role="form"] > * > form > input[type="text"]'));
}
5 changes: 5 additions & 0 deletions __tests__/setup/pageObjects/getUploadButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { By } from 'selenium-webdriver';

export default async function getUploadButton(driver) {
return await driver.findElement(By.css('input[type="file"]'));
}
6 changes: 6 additions & 0 deletions __tests__/setup/pageObjects/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import dispatchAction from './dispatchAction';
import endSpeechSynthesize from './endSpeechSynthesize';
import executePromiseScript from './executePromiseScript';
import getActivityElements from './getActivityElements';
import getMicrophoneButton from './getMicrophoneButton';
import getSendBoxTextBox from './getSendBoxTextBox';
import getStore from './getStore';
import getUploadButton from './getUploadButton';
import hasPendingSpeechSynthesisUtterance from './hasPendingSpeechSynthesisUtterance';
import isRecognizingSpeech from './isRecognizingSpeech';
import pingBot from './pingBot';
import putSpeechRecognitionResult from './putSpeechRecognitionResult';
import sendFile from './sendFile';
import sendMessageViaMicrophone from './sendMessageViaMicrophone';
import sendMessageViaSendBox from './sendMessageViaSendBox';
import startSpeechSynthesize from './startSpeechSynthesize';
Expand All @@ -26,13 +29,16 @@ export default function pageObjects(driver) {
dispatchAction,
endSpeechSynthesize,
executePromiseScript,
getActivityElements,
getMicrophoneButton,
getSendBoxTextBox,
getStore,
getUploadButton,
hasPendingSpeechSynthesisUtterance,
isRecognizingSpeech,
pingBot,
putSpeechRecognitionResult,
sendFile,
sendMessageViaMicrophone,
sendMessageViaSendBox,
startSpeechSynthesize
Expand Down
28 changes: 28 additions & 0 deletions __tests__/setup/pageObjects/sendFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { join, posix } from 'path';
import { timeouts } from '../../constants.json';
import allOutgoingActivitiesSent from '../conditions/allOutgoingActivitiesSent';
import getActivityElements from './getActivityElements';
import getUploadButton from './getUploadButton';
import minNumActivitiesShown from '../conditions/minNumActivitiesShown.js';

function resolveDockerFile(filename) {
return posix.join('/~/Downloads', filename);
}

function resolveLocalFile(filename) {
return join(__dirname, '../local', filename);
}

export default async function sendFile(driver, filename, { waitForSend = true } = {}) {
const uploadButton = await getUploadButton(driver);
const isUnderDocker = !!(await driver.getCapabilities()).get('webdriver.remote.sessionid');

// The send file function is asynchronous, it doesn't send immediately until thumbnails are generated.
// We will save the numActivities, anticipate for numActivities + 1, then wait until everything is sent
const numActivities = (await getActivityElements(driver)).length;

await uploadButton.sendKeys(isUnderDocker ? resolveDockerFile(filename) : resolveLocalFile(filename));

await driver.wait(minNumActivitiesShown(numActivities + 1));
waitForSend && (await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine));
}
169 changes: 169 additions & 0 deletions __tests__/upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { imageSnapshotOptions, timeouts } from './constants.json';

import allImagesLoaded from './setup/conditions/allImagesLoaded';
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);

describe('upload a picture', () => {
test('', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('with custom thumbnail size', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
uploadThumbnailContentType: 'image/png',
uploadThumbnailHeight: 60,
uploadThumbnailWidth: 120
}
}
});

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

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('with custom thumbnail quality', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
uploadThumbnailQuality: 0.1
}
}
});

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

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('with custom thumbnail disabled', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
enableUploadThumbnail: false
}
}
});

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

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

describe('without Web Worker', () => {
test('', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.executeScript(() => {
window.Worker = undefined;
});
await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('with custom thumbnail size', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
uploadThumbnailContentType: 'image/png',
uploadThumbnailHeight: 60,
uploadThumbnailWidth: 120
}
}
});

await driver.executeScript(() => {
window.Worker = undefined;
});
await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('with custom thumbnail quality', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: {
uploadThumbnailQuality: 0.1
}
}
});

await driver.executeScript(() => {
window.Worker = undefined;
});
await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.sendFile('seaofthieves.jpg');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

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

test('upload a ZIP file', async () => {
const { driver, pageObjects } = await setupWebDriver();

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

await pageObjects.sendFile('empty.zip');
await driver.wait(minNumActivitiesShown(2));
await driver.wait(allImagesLoaded());

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ services:
# On Windows, run with COMPOSE_CONVERT_WINDOWS_PATHS=1

chrome:
# https://github.com/SeleniumHQ/docker-selenium
# https://hub.docker.com/r/selenium/standalone-chrome/tags/
image: selenium/standalone-chrome:3.141.59-radium
build:
context: ./
dockerfile: Dockerfile-selenium
networks:
- selenium
depends_on:
Expand Down
1 change: 1 addition & 0 deletions packages/bundle/src/index-es5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'core-js/modules/es.math.sign';
import 'core-js/modules/es.number.is-finite';
import 'core-js/modules/es.object.assign';
import 'core-js/modules/es.promise';
import 'core-js/modules/es.promise.finally';
import 'core-js/modules/es.string.starts-with';
import 'core-js/modules/es.symbol';
import 'url-search-params-polyfill';
Expand Down
14 changes: 14 additions & 0 deletions packages/component/.babelrc
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{
"env": {
"test": {
"exclude": [
"src/**/*.worker.js"
],
"plugins": [
"babel-plugin-istanbul"
]
}
},
"overrides": [{
"test": "src/**/*.worker.js",
"plugins": [],
"presets": [
["@babel/preset-env", {
"targets": {
"chrome": "69"
}
}]
]
}],
"plugins": [
"@babel/proposal-object-rest-spread",
[
Expand Down
17 changes: 16 additions & 1 deletion packages/component/src/Attachment/ImageAttachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,24 @@ import React from 'react';

import ImageContent from './ImageContent';

const ImageAttachment = ({ attachment }) => <ImageContent alt={attachment.name} src={attachment.contentUrl} />;
const ImageAttachment = ({ activity, attachment }) => {
const { attachmentThumbnails } = activity.channelData || {};

if (attachmentThumbnails) {
const attachmentThumbnail = attachmentThumbnails[activity.attachments.indexOf(attachment)];

if (attachmentThumbnail) {
return <ImageContent alt={attachment.name} src={attachmentThumbnail} />;
}
}

return <ImageContent alt={attachment.name} src={attachment.contentUrl} />;
};

ImageAttachment.propTypes = {
activity: PropTypes.shape({
attachments: PropTypes.array.isRequired
}).isRequired,
attachment: PropTypes.shape({
contentUrl: PropTypes.string.isRequired,
name: PropTypes.string
Expand Down
2 changes: 1 addition & 1 deletion packages/component/src/Dictation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React from 'react';
import connectToWebChat from './connectToWebChat';

const {
DictateState: { DICTATING, IDLE, STARTING, STOPPING }
DictateState: { DICTATING, IDLE, STARTING }
} = Constants;

class Dictation extends React.Component {
Expand Down
Loading