diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1c1cecf8f..df0ec62747 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixes [#2822](https://github.com/microsoft/BotFramework-WebChat/issues/2822). Fixed `credentials` should return `authorizationToken` and `subscriptionKey` as string and allow empty LUIS reference grammar ID, by [@compulim](https://github.com/compulim) in PR [#2824](https://github.com/microsoft/BotFramework-WebChat/pull/2824)
- Fixes [#2751](https://github.com/microsoft/BotFramework-WebChat/issues/2751). Move documentation to docs folder, by [@corinagum](https://github.com/corinagum) in PR [#2832](https://github.com/microsoft/BotFramework-WebChat/pull/2832)
- Fixes [#2838](https://github.com/microsoft/BotFramework-WebChat/issues/2838). Fixed `concatMiddleware` should allow any middleware to call its downstream middleware twice, by [@compulim](https://github.com/compulim) in PR [#2839](https://github.com/microsoft/BotFramework-WebChat/pull/2839)
+- Fixes [#2864](https://github.com/microsoft/BotFramework-WebChat/issues/2864). Replaced `DownloadAttachment` and `UploadAttachment` with `FileAttachment`, which shows the download link and icon if the attachment contains the `contentUrl`, by [@compulim](https://github.com/compulim) in PR [#2868](https://github.com/microsoft/BotFramework-WebChat/pull/2868)
### Changed
diff --git a/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-with-content-url-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-with-content-url-1-snap.png
new file mode 100644
index 0000000000..c0a0e79f6b
Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-with-content-url-1-snap.png differ
diff --git a/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-without-content-url-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-without-content-url-1-snap.png
new file mode 100644
index 0000000000..4061132696
Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/file-attachment-js-show-zip-files-without-content-url-1-snap.png differ
diff --git a/__tests__/attachmentMiddleware.js b/__tests__/attachmentMiddleware.js
index ba08ca39dd..1821f2b20f 100644
--- a/__tests__/attachmentMiddleware.js
+++ b/__tests__/attachmentMiddleware.js
@@ -45,7 +45,9 @@ test('file upload should show thumbnail and file name', async () => {
return next({ activity, attachment });
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.wait(uiConnected(), timeouts.directLine);
diff --git a/__tests__/fileAttachment.js b/__tests__/fileAttachment.js
new file mode 100644
index 0000000000..feb080ced9
--- /dev/null
+++ b/__tests__/fileAttachment.js
@@ -0,0 +1,114 @@
+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);
+
+test('show ZIP files with contentUrl', async () => {
+ const { driver, pageObjects } = await setupWebDriver({
+ createDirectLine: options => {
+ const directLine = window.WebChat.createDirectLine(options);
+ const patchedDirectLine = {
+ activity$: new Observable(observer => {
+ const subscription = directLine.activity$.subscribe({
+ next(activity) {
+ observer.next(
+ Object.assign({}, activity, {
+ attachments: (activity.attachments || []).map(attachment =>
+ Object.assign({}, attachment, {
+ contentUrl: 'https://example.org/'
+ })
+ )
+ })
+ );
+ }
+ });
+
+ return () => subscription.unsubscribe();
+ }),
+
+ connectionStatus$: directLine.connectionStatus$,
+ postActivity: directLine.postActivity.bind(directLine),
+ token: directLine.token
+ };
+
+ return patchedDirectLine;
+ }
+ });
+
+ await driver.wait(uiConnected(), timeouts.directLine);
+
+ await pageObjects.sendFile('empty.zip');
+ await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(allImagesLoaded(), timeouts.fetchImage);
+
+ const base64PNG = await driver.takeScreenshot();
+
+ expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
+
+ await expect(
+ driver.executeScript(() =>
+ document.querySelector('[role="listitem"]:nth-child(1) a[target="_blank"]').getAttribute('href')
+ )
+ ).resolves.toEqual('https://example.org/');
+ await expect(
+ driver.executeScript(() =>
+ document.querySelector('[role="listitem"]:nth-child(2) a[target="_blank"]').getAttribute('href')
+ )
+ ).resolves.toEqual('https://example.org/');
+});
+
+test('show ZIP files without contentUrl', async () => {
+ const { driver, pageObjects } = await setupWebDriver({
+ createDirectLine: options => {
+ const directLine = window.WebChat.createDirectLine(options);
+ const patchedDirectLine = {
+ activity$: new Observable(observer => {
+ const subscription = directLine.activity$.subscribe({
+ next(activity) {
+ observer.next(
+ Object.assign({}, activity, {
+ attachments: (activity.attachments || []).map(attachment =>
+ Object.assign({}, attachment, {
+ contentUrl: undefined
+ })
+ )
+ })
+ );
+ }
+ });
+
+ return () => subscription.unsubscribe();
+ }),
+
+ connectionStatus$: directLine.connectionStatus$,
+ postActivity: directLine.postActivity.bind(directLine),
+ token: directLine.token
+ };
+
+ return patchedDirectLine;
+ }
+ });
+
+ await driver.wait(uiConnected(), timeouts.directLine);
+
+ await pageObjects.sendFile('empty.zip');
+ await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(allImagesLoaded(), timeouts.fetchImage);
+
+ const base64PNG = await driver.takeScreenshot();
+
+ expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
+
+ await expect(
+ driver.executeScript(() => !!document.querySelector('[role="listitem"]:nth-child(1) a'))
+ ).resolves.toBeFalsy();
+ await expect(
+ driver.executeScript(() => !!document.querySelector('[role="listitem"]:nth-child(2) a'))
+ ).resolves.toBeFalsy();
+});
diff --git a/__tests__/hooks/useSendFiles.js b/__tests__/hooks/useSendFiles.js
index 8784140635..52863f1c68 100644
--- a/__tests__/hooks/useSendFiles.js
+++ b/__tests__/hooks/useSendFiles.js
@@ -1,5 +1,6 @@
import { imageSnapshotOptions, timeouts } from '../constants.json';
+import allOutgoingActivitiesSent from '../setup/conditions/allOutgoingActivitiesSent';
import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
import uiConnected from '../setup/conditions/uiConnected';
@@ -9,7 +10,10 @@ import uiConnected from '../setup/conditions/uiConnected';
jest.setTimeout(timeouts.test);
test('calling sendFile should send files', async () => {
- const { driver, pageObjects } = await setupWebDriver();
+ const { driver, pageObjects } = await setupWebDriver({
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
+ });
await driver.wait(uiConnected(), timeouts.directLine);
@@ -24,6 +28,7 @@ test('calling sendFile should send files', async () => {
});
await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/hooks/useSendPostBack.js b/__tests__/hooks/useSendPostBack.js
index 68908b0fb9..fee6eedc42 100644
--- a/__tests__/hooks/useSendPostBack.js
+++ b/__tests__/hooks/useSendPostBack.js
@@ -15,7 +15,7 @@ test('calling sendPostBack should send a post back activity', async () => {
await pageObjects.runHook('useSendPostBack', [], sendPostBack => sendPostBack({ hello: 'World!' }));
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(1), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/language.js b/__tests__/language.js
index 63504410ed..de223f9f67 100644
--- a/__tests__/language.js
+++ b/__tests__/language.js
@@ -14,7 +14,7 @@ test('setting language to empty string should not crash', async () => {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo Hello', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/markdown.js b/__tests__/markdown.js
index 4a22c5277a..95fa1ddd31 100644
--- a/__tests__/markdown.js
+++ b/__tests__/markdown.js
@@ -29,7 +29,7 @@ test('null renderMarkdown function', async () => {
await pageObjects.sendMessageViaSendBox('echo **This text should be plain text**', { waitForSend: true });
await driver.wait(allImagesLoaded(), timeouts.fetchImage);
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/sendBox.js b/__tests__/sendBox.js
index d8117558a8..b3485ec201 100644
--- a/__tests__/sendBox.js
+++ b/__tests__/sendBox.js
@@ -26,7 +26,7 @@ test('should focus send box when message is being sent', async () => {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('Hello, World!', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(1), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/sendTypingIndicator.js b/__tests__/sendTypingIndicator.js
index a7b5c43305..2870fe8b96 100644
--- a/__tests__/sendTypingIndicator.js
+++ b/__tests__/sendTypingIndicator.js
@@ -67,7 +67,7 @@ test('typing indicator should not display after second activity', async () => {
});
await pageObjects.sendMessageViaSendBox('typing', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
diff --git a/__tests__/setup/conditions/minNumActivitiesShown.js b/__tests__/setup/conditions/minNumActivitiesShown.js
index f708cfb7e5..bd26f3f15b 100644
--- a/__tests__/setup/conditions/minNumActivitiesShown.js
+++ b/__tests__/setup/conditions/minNumActivitiesShown.js
@@ -1,5 +1,19 @@
-import { By, until } from 'selenium-webdriver';
+import { Condition } from 'selenium-webdriver';
export default function minNumActivitiesShown(numActivities) {
- return until.elementLocated(By.css(`[role="listitem"]:nth-child(${numActivities})`));
+ return new Condition(`${numActivities} activities is shown`, async driver => {
+ // To run hooks (WebChatTest.runHook), the code internally create an activity.
+ // Inside the activity renderer, it call hooks, but return empty visually.
+ // This activity is invisible and should not count towards "minNumActivitiesShown".
+
+ const numActivitiesShown = await driver.executeScript(() =>
+ [].reduce.call(
+ document.querySelectorAll('[role="listitem"]'),
+ (numActivitiesShown, child) => numActivitiesShown + (child.children.length ? 1 : 0),
+ 0
+ )
+ );
+
+ return numActivitiesShown >= numActivities;
+ });
}
diff --git a/__tests__/speech.selectVoice.js b/__tests__/speech.selectVoice.js
index 92b0ac2d40..31e7e5a1f2 100644
--- a/__tests__/speech.selectVoice.js
+++ b/__tests__/speech.selectVoice.js
@@ -20,7 +20,7 @@ describe('selecting voice based on language', () => {
await pageObjects.sendMessageViaMicrophone('echo 123');
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await driver.wait(speechSynthesisUtterancePended(), timeouts.ui);
await expect(pageObjects.startSpeechSynthesize()).resolves.toHaveProperty('voice', {
@@ -42,7 +42,7 @@ describe('selecting voice based on language', () => {
await pageObjects.sendMessageViaMicrophone('echo 123');
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await driver.wait(speechSynthesisUtterancePended(), timeouts.ui);
await expect(pageObjects.startSpeechSynthesize()).resolves.toHaveProperty('voice', {
@@ -66,7 +66,7 @@ describe('selecting voice based on language', () => {
await pageObjects.sendMessageViaMicrophone('echo 123');
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await driver.wait(speechSynthesisUtterancePended(), timeouts.ui);
await expect(pageObjects.startSpeechSynthesize()).resolves.toHaveProperty('voice', {
diff --git a/__tests__/speech.synthesis.js b/__tests__/speech.synthesis.js
index 52fbf582a3..40cbf82776 100644
--- a/__tests__/speech.synthesis.js
+++ b/__tests__/speech.synthesis.js
@@ -110,7 +110,7 @@ describe('speech synthesis', () => {
await pageObjects.sendMessageViaMicrophone('echo Hello, World!');
await expect(speechRecognitionStartCalled().fn(driver)).resolves.toBeFalsy();
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await expect(pageObjects.startSpeechSynthesize()).resolves.toHaveProperty(
'text',
@@ -140,7 +140,7 @@ describe('speech synthesis', () => {
}
});
- await pageObjects.sendMessageViaMicrophone('input hint expected');
+ await pageObjects.sendMessageViaMicrophone('hint expected');
await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
diff --git a/__tests__/styleOptions.js b/__tests__/styleOptions.js
index 74ef198523..cd3c985a0a 100644
--- a/__tests__/styleOptions.js
+++ b/__tests__/styleOptions.js
@@ -35,7 +35,7 @@ describe('style options', () => {
waitForSend: true
});
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});
diff --git a/__tests__/suggestedActions.js b/__tests__/suggestedActions.js
index 1c04c9a7b7..6fd20b402a 100644
--- a/__tests__/suggestedActions.js
+++ b/__tests__/suggestedActions.js
@@ -57,7 +57,7 @@ describe('suggested-actions command', () => {
const imBackButton = buttons[1];
await imBackButton.click();
- await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(4), timeouts.directLine);
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
@@ -120,7 +120,7 @@ describe('suggested-actions command', () => {
const postBackStringButton = buttons[4];
await postBackStringButton.click();
- await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(4), timeouts.directLine);
await driver.wait(allOutgoingActivitiesSent(), timeouts.directLine);
const base64PNG = await driver.takeScreenshot();
diff --git a/__tests__/timestamp.js b/__tests__/timestamp.js
index 5bbb0ccc64..802a0e3ee1 100644
--- a/__tests__/timestamp.js
+++ b/__tests__/timestamp.js
@@ -24,7 +24,7 @@ test('update timestamp language on-the-fly', async () => {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo Hello, World!', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
@@ -83,7 +83,7 @@ test('prepend text', async () => {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo Hello, World!', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});
@@ -94,7 +94,7 @@ test('change timestamp grouping on-the-fly', async () => {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo Hello, World!', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
diff --git a/__tests__/updateActivity.js b/__tests__/updateActivity.js
index c0c511eac2..78ca851aec 100644
--- a/__tests__/updateActivity.js
+++ b/__tests__/updateActivity.js
@@ -78,12 +78,12 @@ test('should not replace activity without activity ID', async () => {
await pageObjects.sendMessageViaSendBox('echo This message will not be replaced by a carousel.', {
waitForSend: true
});
- await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions);
await pageObjects.sendMessageViaSendBox('carousel', { waitForSend: true });
- await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
+ await driver.wait(minNumActivitiesShown(5), timeouts.directLine);
await driver.wait(allImagesLoaded(), timeouts.fetchImage);
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);
diff --git a/__tests__/upload.js b/__tests__/upload.js
index 7c7a589c34..30f4c6942a 100644
--- a/__tests__/upload.js
+++ b/__tests__/upload.js
@@ -11,7 +11,10 @@ jest.setTimeout(timeouts.test);
describe('upload a picture', () => {
test('', async () => {
- const { driver, pageObjects } = await setupWebDriver();
+ const { driver, pageObjects } = await setupWebDriver({
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
+ });
await driver.wait(uiConnected(), timeouts.directLine);
@@ -32,7 +35,9 @@ describe('upload a picture', () => {
uploadThumbnailHeight: 60,
uploadThumbnailWidth: 120
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.wait(uiConnected(), timeouts.directLine);
@@ -52,7 +57,9 @@ describe('upload a picture', () => {
styleOptions: {
uploadThumbnailQuality: 0.1
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.wait(uiConnected(), timeouts.directLine);
@@ -72,7 +79,9 @@ describe('upload a picture', () => {
styleOptions: {
enableUploadThumbnail: false
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.wait(uiConnected(), timeouts.directLine);
@@ -88,7 +97,10 @@ describe('upload a picture', () => {
describe('without Web Worker', () => {
test('', async () => {
- const { driver, pageObjects } = await setupWebDriver();
+ const { driver, pageObjects } = await setupWebDriver({
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
+ });
await driver.executeScript(() => {
window.Worker = undefined;
@@ -112,7 +124,9 @@ describe('upload a picture', () => {
uploadThumbnailHeight: 60,
uploadThumbnailWidth: 120
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.executeScript(() => {
@@ -135,7 +149,9 @@ describe('upload a picture', () => {
styleOptions: {
uploadThumbnailQuality: 0.1
}
- }
+ },
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
});
await driver.executeScript(() => {
@@ -155,7 +171,10 @@ describe('upload a picture', () => {
});
test('upload a ZIP file', async () => {
- const { driver, pageObjects } = await setupWebDriver();
+ const { driver, pageObjects } = await setupWebDriver({
+ // TODO: [P3] Offline bot did not reply with a downloadable attachment, we need to use production bot
+ useProductionBot: true
+ });
await driver.wait(uiConnected(), timeouts.directLine);
diff --git a/packages/component/src/Attachment/DownloadAttachment.js b/packages/component/src/Attachment/DownloadAttachment.js
deleted file mode 100644
index 737f8a9547..0000000000
--- a/packages/component/src/Attachment/DownloadAttachment.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { format } from 'bytes';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import DownloadIcon from './Assets/DownloadIcon';
-import ScreenReaderText from '../ScreenReaderText';
-import useLocalize from '../hooks/useLocalize';
-import useStyleSet from '../hooks/useStyleSet';
-
-const DownloadAttachment = ({
- activity: { attachments = [], channelData: { attachmentSizes = [] } = {} } = {},
- attachment
-}) => {
- const [{ downloadAttachment: downloadAttachmentStyleSet }] = useStyleSet();
- const downloadLabel = useLocalize('Download file');
-
- const attachmentIndex = attachments.indexOf(attachment);
- const size = attachmentSizes[attachmentIndex];
- const formattedSize = typeof size === 'number' && format(size);
-
- const downloadFileWithFileSizeLabel = useLocalize(
- 'DownloadFileWithFileSize',
- downloadLabel,
- attachment.name,
- formattedSize
- );
-
- return (
-