diff --git a/CHANGELOG.md b/CHANGELOG.md index f61785e2c8..7c2c7758ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Resolves [#2753](https://github.com/microsoft/BotFramework-WebChat/issues/2753). Added support for updating an activity by the ID, by [@compulim](https://github.com/compulim) in PR [#2825](https://github.com/microsoft/BotFramework-WebChat/pull/2825) + ### Fixed - Fixes [#2611](https://github.com/microsoft/BotFramework-WebChat/issues/2611). Fix sample 21: hooks errors, by [@corinagum](https://github.com/corinagum) in PR [#2740](https://github.com/microsoft/BotFramework-WebChat/pull/2740) diff --git a/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-1-snap.png new file mode 100644 index 0000000000..7de0d5a9fe Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-2-snap.png new file mode 100644 index 0000000000..614b6f50c2 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-not-replace-activity-without-activity-id-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-1-snap.png new file mode 100644 index 0000000000..cf7d3b480c Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-2-snap.png new file mode 100644 index 0000000000..97f30cc59b Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/update-activity-js-should-replace-activity-with-same-activity-id-2-snap.png differ diff --git a/__tests__/updateActivity.js b/__tests__/updateActivity.js new file mode 100644 index 0000000000..c0c511eac2 --- /dev/null +++ b/__tests__/updateActivity.js @@ -0,0 +1,91 @@ +import { imageSnapshotOptions, timeouts } from './constants.json'; + +import allImagesLoaded from './setup/conditions/allImagesLoaded'; +import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown'; +import scrollToBottomCompleted from './setup/conditions/scrollToBottomCompleted'; +import uiConnected from './setup/conditions/uiConnected'; + +jest.setTimeout(timeouts.test); + +test('should replace activity with same activity ID', async () => { + const { driver, pageObjects } = await setupWebDriver({ + createDirectLine: options => { + const workingDirectLine = window.WebChat.createDirectLine(options); + let firstBotActivityId; + let firstBotActivityTimestamp; + + return { + activity$: workingDirectLine.activity$.map(activity => { + if (!activity.channelData && activity.type === 'message') { + // Duplicate the ID using the first message activity from bot. + + if (firstBotActivityId) { + return Object.assign({}, activity, { + id: firstBotActivityId, + timestamp: firstBotActivityTimestamp + }); + } + + firstBotActivityId = activity.id; + firstBotActivityTimestamp = activity.timestamp; + } + + return activity; + }), + connectionStatus$: workingDirectLine.connectionStatus$, + postActivity: workingDirectLine.postActivity.bind(workingDirectLine), + token: workingDirectLine.token + }; + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('echo This message will be replaced by a carousel.', { waitForSend: true }); + await driver.wait(minNumActivitiesShown(2), 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(allImagesLoaded(), timeouts.fetchImage); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); +}); + +test('should not replace activity without activity ID', async () => { + const { driver, pageObjects } = await setupWebDriver({ + createDirectLine: options => { + const workingDirectLine = window.WebChat.createDirectLine(options); + + return { + activity$: workingDirectLine.activity$.map(activity => { + if (!activity.channelData && activity.type === 'message') { + // Remove all activity ID from bot + + return Object.assign({}, activity, { id: undefined }); + } + + return activity; + }), + connectionStatus$: workingDirectLine.connectionStatus$, + postActivity: workingDirectLine.postActivity.bind(workingDirectLine), + token: workingDirectLine.token + }; + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('echo This message will not be replaced by a carousel.', { + waitForSend: true + }); + await driver.wait(minNumActivitiesShown(2), 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(allImagesLoaded(), timeouts.fetchImage); + await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); +}); diff --git a/packages/core/src/reducers/activities.js b/packages/core/src/reducers/activities.js index 1d6f43f415..cce3206ec6 100644 --- a/packages/core/src/reducers/activities.js +++ b/packages/core/src/reducers/activities.js @@ -25,10 +25,10 @@ function upsertActivityWithSort(activities, nextActivity) { const nextTimestamp = Date.parse(nextActivity.timestamp); const nextActivities = activities.filter( - ({ channelData: { clientActivityID } = {} }) => - // We will remove all "sending messages" activities + ({ channelData: { clientActivityID } = {}, id }) => + // We will remove all "sending messages" activities and activities with same ID // "clientActivityID" is unique and used to track if the message has been sent and echoed back from the server - !(nextClientActivityID && clientActivityID === nextClientActivityID) + !(nextClientActivityID && clientActivityID === nextClientActivityID) && !(id && id === nextActivity.id) ); // Then, find the right (sorted) place to insert the new activity at, based on timestamp @@ -83,9 +83,8 @@ export default function activities(state = DEFAULT_STATE, { meta, payload, type break; case INCOMING_ACTIVITY: - // UpdateActivity is not supported right now because we ignore duplicated activity ID // TODO: [P4] Move "typing" into Constants.ActivityType - if (payload.activity.type !== 'typing' && !~state.findIndex(({ id }) => id === payload.activity.id)) { + if (payload.activity.type !== 'typing') { state = upsertActivityWithSort(state, payload.activity); }