diff --git a/.eslintrc.yml b/.eslintrc.yml index ec1d65cde0..e06098c99a 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -19,14 +19,6 @@ parserOptions: sourceType: module overrides: - - files: - - '__tests__/**/*.js' - - '*.spec.js' - - '*.test.js' - - env: - jest: true - - files: - 'jest.config.js' - 'jest.*.config.js' @@ -85,6 +77,24 @@ overrides: # Shorthanding if-condition with && and ||. '@typescript-eslint/no-unused-expressions': off + - files: + - '__tests__/**/*.js' + - '**/*.spec.js' + - '**/*.spec.jsx' + - '**/*.spec.ts' + - '**/*.spec.tsx' + - '**/*.test.js' + - '**/*.test.jsx' + - '**/*.test.ts' + - '**/*.test.tsx' + + env: + jest: true + + rules: + '@typescript-eslint/no-require-imports': off + no-magic-numbers: off + rules: # Only list rules that are not in *:recommended set # If rules are set to disable the one in *:recommended, please elaborate the reason diff --git a/CHANGELOG.md b/CHANGELOG.md index f0cf52355c..00ca1c2746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ - Added dedicated loading animation for messages in preparing state for Fluent theme, in PR [#5423](https://github.com/microsoft/BotFramework-WebChat/pull/5423), by [@OEvgeny](https://github.com/OEvgeny) - Resolved [#2661](https://github.com/microsoft/BotFramework-WebChat/issues/2661) and [#5352](https://github.com/microsoft/BotFramework-WebChat/issues/5352). Added speech recognition continuous mode with barge-in support, in PR [#5426](https://github.com/microsoft/BotFramework-WebChat/pull/5426), by [@RushikeshGavali](https://github.com/RushikeshGavali) and [@compulim](https://github.com/compulim) - Set `styleOptions.speechRecognitionContinuous` to `true` with a Web Speech API provider with continuous mode support +- Added support of [contentless activity in livestream](https://github.com/microsoft/BotFramework-WebChat/blob/main/docs/LIVESTREAMING.md#scenario-3-interim-activities-with-no-content), in PR [#5430](https://github.com/microsoft/BotFramework-WebChat/pull/5430), by [@compulim](https://github.com/compulim) ### Changed diff --git a/__tests__/__image_snapshots__/html/activity-order-js-bot-sending-multiple-messages-should-sort-typing-activity-in-its-original-order-5-snap.png b/__tests__/__image_snapshots__/html/activity-order-js-bot-sending-multiple-messages-should-sort-typing-activity-in-its-original-order-5-snap.png deleted file mode 100644 index 359ca556cb..0000000000 Binary files a/__tests__/__image_snapshots__/html/activity-order-js-bot-sending-multiple-messages-should-sort-typing-activity-in-its-original-order-5-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/chunk-js-bot-typing-with-chunks-should-display-partial-message-4-snap.png b/__tests__/__image_snapshots__/html/chunk-js-bot-typing-with-chunks-should-display-partial-message-4-snap.png deleted file mode 100644 index b881212408..0000000000 Binary files a/__tests__/__image_snapshots__/html/chunk-js-bot-typing-with-chunks-should-display-partial-message-4-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-2-snap.png b/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-2-snap.png deleted file mode 100644 index d9b90f37b9..0000000000 Binary files a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-2-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-3-snap.png b/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-3-snap.png deleted file mode 100644 index d9b90f37b9..0000000000 Binary files a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-3-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-4-snap.png b/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-4-snap.png deleted file mode 100644 index d9b90f37b9..0000000000 Binary files a/__tests__/__image_snapshots__/html/concluded-livestream-js-bot-reusing-concluded-livestream-session-id-should-be-ignored-4-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/informative-js-informative-typing-message-should-be-shown-as-typing-indicator-1-snap.png b/__tests__/__image_snapshots__/html/informative-js-informative-typing-message-should-be-shown-as-typing-indicator-1-snap.png deleted file mode 100644 index e3d75a1f89..0000000000 Binary files a/__tests__/__image_snapshots__/html/informative-js-informative-typing-message-should-be-shown-as-typing-indicator-1-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-1-snap.png b/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-1-snap.png deleted file mode 100644 index 9299540e81..0000000000 Binary files a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-1-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-2-snap.png b/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-2-snap.png deleted file mode 100644 index d73a87ed0d..0000000000 Binary files a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-2-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-3-snap.png b/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-3-snap.png deleted file mode 100644 index 172c91d0a3..0000000000 Binary files a/__tests__/__image_snapshots__/html/layout-js-livestreaming-should-layout-properly-3-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/out-of-order-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-in-its-original-order-4-snap.png b/__tests__/__image_snapshots__/html/out-of-order-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-in-its-original-order-4-snap.png deleted file mode 100644 index b881212408..0000000000 Binary files a/__tests__/__image_snapshots__/html/out-of-order-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-in-its-original-order-4-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/out-of-order-sequence-number-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-based-on-channel-data-sequence-number-4-snap.png b/__tests__/__image_snapshots__/html/out-of-order-sequence-number-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-based-on-channel-data-sequence-number-4-snap.png deleted file mode 100644 index b881212408..0000000000 Binary files a/__tests__/__image_snapshots__/html/out-of-order-sequence-number-js-bot-typing-message-in-out-of-order-fashion-should-sort-typing-activity-based-on-channel-data-sequence-number-4-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-5-snap.png b/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-5-snap.png deleted file mode 100644 index 19dd3dc767..0000000000 Binary files a/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-5-snap.png and /dev/null differ diff --git a/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-6-snap.png b/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-6-snap.png deleted file mode 100644 index 0e66ecafdd..0000000000 Binary files a/__tests__/__image_snapshots__/html/simultaneous-js-bot-typing-multiple-messages-should-work-properly-6-snap.png and /dev/null differ diff --git a/__tests__/basic.js b/__tests__/basic.js index 10812eceab..03caed7c29 100644 --- a/__tests__/basic.js +++ b/__tests__/basic.js @@ -22,184 +22,3 @@ test('setup', async () => { expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); }); - -test('long URLs with break-word', async () => { - const { driver, pageObjects } = await setupWebDriver(); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox( - 'https://subdomain.domain.com/pathname0/pathname1/pathname2/pathname3/pathname4/', - { waitForSend: true } - ); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('long URLs with break-all', async () => { - const WEB_CHAT_PROPS = { styleOptions: { messageActivityWordBreak: 'break-all' } }; - - const { driver, pageObjects } = await setupWebDriver({ props: WEB_CHAT_PROPS }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox( - 'https://subdomain.domain.com/pathname0/pathname1/pathname2/pathname3/pathname4/', - { waitForSend: true } - ); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('long URLs with keep-all', async () => { - const WEB_CHAT_PROPS = { styleOptions: { messageActivityWordBreak: 'keep-all' } }; - - const { driver, pageObjects } = await setupWebDriver({ props: WEB_CHAT_PROPS }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('箸より重いものを持ったことがない箸より重いものを持ったことがない', { - waitForSend: true - }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('hero card with a long title and richCardWrapTitle set to true', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions: { richCardWrapTitle: true } } }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('herocard long title', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('hero card with a long title and richCardWrapTitle set to default value', async () => { - const { driver, pageObjects } = await setupWebDriver(); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('herocard long title', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('thumbnail card with a long title and richCardWrapTitle set to true', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions: { richCardWrapTitle: true } } }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('thumbnailcard long title', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('thumbnail card with a long title and richCardWrapTitle set to default value', async () => { - const { driver, pageObjects } = await setupWebDriver(); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('thumbnailcard long title', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(allImagesLoaded(), timeouts.fetchImage); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('absolute timestamp', async () => { - const activities = [ - { - channelData: {}, - type: 'message', - id: '6266x5ZXhXkBfuIH0fNx0h-o|0000000', - timestamp: '2019-08-08T16:41:12.9397263Z', - from: { - id: 'dl_654b35e09ab4149595a70aa6f1af6f50', - name: '', - role: 'user' - }, - textFormat: 'plain', - text: 'echo "Hello, World!"' - }, - { - channelData: {}, - type: 'message', - id: '6266x5ZXhXkBfuIH0fNx0h-o|0000001', - timestamp: '2019-08-08T16:41:13.1835518Z', - from: { - id: 'webchat-mockbot', - name: 'webchat-mockbot', - role: 'bot' - }, - text: 'Echoing back in a separate activity.' - }, - { - channelData: {}, - type: 'message', - id: '6266x5ZXhXkBfuIH0fNx0h-o|0000002', - timestamp: '2019-08-08T16:41:13.3963019Z', - from: { - id: 'webchat-mockbot', - name: 'webchat-mockbot', - role: 'bot' - }, - text: 'Hello, World!' - } - ]; - const styleOptions = { timestampFormat: 'absolute' }; - const { driver } = await setupWebDriver({ storeInitialState: { activities }, props: { styleOptions } }); - - await driver.wait(uiConnected(), timeouts.directLine); - await driver.wait(minNumActivitiesShown(3), timeouts.directLine); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('avatar background color', async () => { - const styleOptions = { - botAvatarBackgroundColor: 'red', - botAvatarInitials: 'B', - userAvatarBackgroundColor: 'blue', - userAvatarInitials: 'TJ' - }; - - const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions } }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('echo "Hello, World!"', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(3), timeouts.directLine); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); diff --git a/__tests__/hooks/useEmitTypingIndicator.js b/__tests__/hooks/useEmitTypingIndicator.js deleted file mode 100644 index 0eaee6a1f9..0000000000 --- a/__tests__/hooks/useEmitTypingIndicator.js +++ /dev/null @@ -1,23 +0,0 @@ -import { timeouts } from '../constants.json'; - -import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown'; -import typingActivityReceived from '../setup/conditions/typingActivityReceived'; -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('calling emitTypingIndicator should send a typing activity', async () => { - const { driver, pageObjects } = await setupWebDriver(); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - - await pageObjects.runHook('useEmitTypingIndicator', [], fn => fn()); - - await driver.wait(typingActivityReceived(), timeouts.directLine); -}); diff --git a/__tests__/hooks/useSendTypingIndicator.js b/__tests__/hooks/useSendTypingIndicator.js deleted file mode 100644 index 7df44dc1a1..0000000000 --- a/__tests__/hooks/useSendTypingIndicator.js +++ /dev/null @@ -1,25 +0,0 @@ -import { timeouts } from '../constants.json'; - -// selenium-webdriver API doc: -// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html - -jest.setTimeout(timeouts.test); - -test('getter should get sendTypingIndicator from props', async () => { - const { pageObjects } = await setupWebDriver({ props: { sendTypingIndicator: true } }); - - await expect(pageObjects.runHook('useSendTypingIndicator', [], result => result[0])).resolves.toBeTruthy(); -}); - -test('getter should get default value if not set in props', async () => { - const { pageObjects } = await setupWebDriver(); - - await expect(pageObjects.runHook('useSendTypingIndicator', [], result => result[0])).resolves.toBeFalsy(); -}); - -test('setter should be falsy', async () => { - const { pageObjects } = await setupWebDriver(); - const [_, setSendTypingIndicator] = await pageObjects.runHook('useSendTypingIndicator'); - - expect(setSendTypingIndicator).toBeFalsy(); -}); diff --git a/__tests__/html/conversationStartProperties.noLocaleIsSent.html b/__tests__/html/conversationStartProperties.noLocaleIsSent.html index 3b5ded88c6..b673b24242 100644 --- a/__tests__/html/conversationStartProperties.noLocaleIsSent.html +++ b/__tests__/html/conversationStartProperties.noLocaleIsSent.html @@ -24,7 +24,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/conversationStartProperties.sendEnUs.html b/__tests__/html/conversationStartProperties.sendEnUs.html index 8083d75c27..74821374f9 100644 --- a/__tests__/html/conversationStartProperties.sendEnUs.html +++ b/__tests__/html/conversationStartProperties.sendEnUs.html @@ -27,7 +27,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/conversationStartProperties.sendInvalidType.html b/__tests__/html/conversationStartProperties.sendInvalidType.html index a9ff9bff46..4dceace762 100644 --- a/__tests__/html/conversationStartProperties.sendInvalidType.html +++ b/__tests__/html/conversationStartProperties.sendInvalidType.html @@ -27,7 +27,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/conversationStartProperties.sendNonExisting.html b/__tests__/html/conversationStartProperties.sendNonExisting.html index 8437c39037..84d28b24ea 100644 --- a/__tests__/html/conversationStartProperties.sendNonExisting.html +++ b/__tests__/html/conversationStartProperties.sendNonExisting.html @@ -27,7 +27,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/conversationStartProperties.sendNonISOFormat.html b/__tests__/html/conversationStartProperties.sendNonISOFormat.html index 2de71308d8..edab8d154c 100644 --- a/__tests__/html/conversationStartProperties.sendNonISOFormat.html +++ b/__tests__/html/conversationStartProperties.sendNonISOFormat.html @@ -27,7 +27,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/conversationStartProperties.sendZhCn.html b/__tests__/html/conversationStartProperties.sendZhCn.html index 5a806c1e1b..7cbcc79306 100644 --- a/__tests__/html/conversationStartProperties.sendZhCn.html +++ b/__tests__/html/conversationStartProperties.sendZhCn.html @@ -27,7 +27,7 @@ await pageConditions.uiConnected(); await pageObjects.sendMessageViaSendBox('conversationstart'); - await pageConditions.minNumActivitiesShown(1); + await pageConditions.minNumActivitiesShown(2); await host.snapshot(); }); diff --git a/__tests__/html/hooks.useActiveTyping.html b/__tests__/html/hooks.useActiveTyping.html index 26f07fc6ba..49897719fc 100644 --- a/__tests__/html/hooks.useActiveTyping.html +++ b/__tests__/html/hooks.useActiveTyping.html @@ -90,22 +90,28 @@ await pageObjects.typeInSendBox('.'); // THEN: `useActiveTyping` should return both. - await expect(renderWithFunction(() => Object.values(useActiveTyping()[0]))).resolves.toEqual([ - { - at: 600, - expireAt: 5600, - name: expect.any(String), - role: 'bot', - type: 'busy' - }, - { - at: 600, - expireAt: 5600, - name: expect.any(String), - role: 'user', - type: 'busy' - } - ]); + const hookResult = await renderWithFunction(() => Object.values(useActiveTyping()[0])); + + expect(hookResult).toHaveLength(2); + + expect(hookResult).toEqual( + expect.arrayContaining([ + { + at: 600, + expireAt: 5600, + name: expect.any(String), + role: 'bot', + type: 'busy' + }, + { + at: 600, + expireAt: 5600, + name: expect.any(String), + role: 'user', + type: 'busy' + } + ]) + ); }); diff --git a/__tests__/html/hooks.useActiveTyping.livestream.html b/__tests__/html/hooks.useActiveTyping.livestream.html index a191e61881..64c96c3df0 100644 --- a/__tests__/html/hooks.useActiveTyping.livestream.html +++ b/__tests__/html/hooks.useActiveTyping.livestream.html @@ -74,7 +74,7 @@ { bot: { at: 600, - expireAt: 5600, + expireAt: Infinity, name: 'Bot', role: 'bot', type: 'livestream' diff --git a/__tests__/html/hooks.useActiveTyping.variable.js b/__tests__/html/hooks.useActiveTyping.variable.js deleted file mode 100644 index d750821f8b..0000000000 --- a/__tests__/html/hooks.useActiveTyping.variable.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('useActiveTyping', () => { - test('should support variable timing', () => runHTML('hooks.useActiveTyping.variable.html')); -}); diff --git a/__tests__/html/typing/activityOrder.js b/__tests__/html/typing/activityOrder.js deleted file mode 100644 index fcbd2a8e48..0000000000 --- a/__tests__/html/typing/activityOrder.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot sending multiple messages', () => { - test('should sort typing activity in its original order', () => runHTML('typing/activityOrder')); -}); diff --git a/__tests__/html/typing/chunk.js b/__tests__/html/typing/chunk.js deleted file mode 100644 index cb795c1f3b..0000000000 --- a/__tests__/html/typing/chunk.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot typing with chunks', () => { - test('should display partial message', () => runHTML('typing/chunk')); -}); diff --git a/__tests__/html/typing/concludedLivestream.js b/__tests__/html/typing/concludedLivestream.js deleted file mode 100644 index 2eff87886c..0000000000 --- a/__tests__/html/typing/concludedLivestream.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot reusing concluded livestream session ID', () => { - test('should be ignored', () => runHTML('typing/concludedLivestream')); -}); diff --git a/__tests__/html/typing/informative.js b/__tests__/html/typing/informative.js deleted file mode 100644 index 742379c45e..0000000000 --- a/__tests__/html/typing/informative.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('informative typing message', () => { - test('should be shown as typing indicator', () => runHTML('typing/informative')); -}); diff --git a/__tests__/html/typing/layout.js b/__tests__/html/typing/layout.js deleted file mode 100644 index 0c0f1677ff..0000000000 --- a/__tests__/html/typing/layout.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('Livestreaming', () => { - test('should layout properly', () => runHTML('typing/layout')); -}); diff --git a/__tests__/html/typing/outOfOrder.js b/__tests__/html/typing/outOfOrder.js deleted file mode 100644 index 5e5d8ab0f0..0000000000 --- a/__tests__/html/typing/outOfOrder.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot typing message in out-of-order fashion', () => { - test('should sort typing activity in its original order', () => runHTML('typing/outOfOrder')); -}); diff --git a/__tests__/html/typing/outOfOrder.sequenceNumber.js b/__tests__/html/typing/outOfOrder.sequenceNumber.js deleted file mode 100644 index ac2e421ac2..0000000000 --- a/__tests__/html/typing/outOfOrder.sequenceNumber.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot typing message in out-of-order fashion', () => { - test('should sort typing activity based on channelData.sequenceNumber', () => runHTML('typing/outOfOrder.sequenceNumber')); -}); diff --git a/__tests__/html/typing/perActivityStyleOptions.js b/__tests__/html/typing/perActivityStyleOptions.js deleted file mode 100644 index 2175f83b8f..0000000000 --- a/__tests__/html/typing/perActivityStyleOptions.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot typing message with a custom typing indicator in channelData', () => { - test('should only show/hide typing indicator accordingly', () => runHTML('typing/perActivityStyleOptions')); -}); diff --git a/__tests__/html/typing/simultaneous.js b/__tests__/html/typing/simultaneous.js deleted file mode 100644 index c467feadc0..0000000000 --- a/__tests__/html/typing/simultaneous.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('bot typing multiple messages', () => { - test('should work properly', () => runHTML('typing/simultaneous')); -}); diff --git a/__tests__/html/typing/typingIndicator.shouldNotRevive.js b/__tests__/html/typing/typingIndicator.shouldNotRevive.js deleted file mode 100644 index a506e9bf39..0000000000 --- a/__tests__/html/typing/typingIndicator.shouldNotRevive.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ - -describe('expired typing indicator', () => { - test('should not revive when an OOO message is received', () => runHTML('typing/typingIndicator.shouldNotRevive')); -}); diff --git a/__tests__/html/typingIndicator.liveRegion.multiple.html b/__tests__/html/typingIndicator.liveRegion.multiple.html index 2e87d1fa7b..f0dfae4c93 100644 --- a/__tests__/html/typingIndicator.liveRegion.multiple.html +++ b/__tests__/html/typingIndicator.liveRegion.multiple.html @@ -1,4 +1,4 @@ - + @@ -8,7 +8,16 @@
- + + + + + + + + +
+ + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-absolute-timestamp-1-snap.png b/__tests__/html2/basic/absoluteTimestamp.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-absolute-timestamp-1-snap.png rename to __tests__/html2/basic/absoluteTimestamp.html.snap-1.png diff --git a/__tests__/html2/basic/assets/surface1.jpg b/__tests__/html2/basic/assets/surface1.jpg new file mode 100644 index 0000000000..5fe603bb54 Binary files /dev/null and b/__tests__/html2/basic/assets/surface1.jpg differ diff --git a/__tests__/html2/basic/avatarBackgroundColor.html b/__tests__/html2/basic/avatarBackgroundColor.html new file mode 100644 index 0000000000..e2d198d964 --- /dev/null +++ b/__tests__/html2/basic/avatarBackgroundColor.html @@ -0,0 +1,64 @@ + + + + + + + + + + + +
+ + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-avatar-background-color-1-snap.png b/__tests__/html2/basic/avatarBackgroundColor.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-avatar-background-color-1-snap.png rename to __tests__/html2/basic/avatarBackgroundColor.html.snap-1.png diff --git a/__tests__/html2/basic/heroCard.wrapTitle.html b/__tests__/html2/basic/heroCard.wrapTitle.html new file mode 100644 index 0000000000..80704a55df --- /dev/null +++ b/__tests__/html2/basic/heroCard.wrapTitle.html @@ -0,0 +1,89 @@ + + + + + + + + + + + +
+ + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-hero-card-with-a-long-title-and-rich-card-wrap-title-set-to-default-value-1-snap.png b/__tests__/html2/basic/heroCard.wrapTitle.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-hero-card-with-a-long-title-and-rich-card-wrap-title-set-to-default-value-1-snap.png rename to __tests__/html2/basic/heroCard.wrapTitle.html.snap-1.png diff --git a/__tests__/html2/basic/heroCard.wrapTitle.wrap.html b/__tests__/html2/basic/heroCard.wrapTitle.wrap.html new file mode 100644 index 0000000000..46b7297119 --- /dev/null +++ b/__tests__/html2/basic/heroCard.wrapTitle.wrap.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-hero-card-with-a-long-title-and-rich-card-wrap-title-set-to-true-1-snap.png b/__tests__/html2/basic/heroCard.wrapTitle.wrap.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-hero-card-with-a-long-title-and-rich-card-wrap-title-set-to-true-1-snap.png rename to __tests__/html2/basic/heroCard.wrapTitle.wrap.html.snap-1.png diff --git a/__tests__/html2/basic/longURL.breakAll.html b/__tests__/html2/basic/longURL.breakAll.html new file mode 100644 index 0000000000..c0207715a4 --- /dev/null +++ b/__tests__/html2/basic/longURL.breakAll.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-break-all-1-snap.png b/__tests__/html2/basic/longURL.breakAll.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-break-all-1-snap.png rename to __tests__/html2/basic/longURL.breakAll.html.snap-1.png diff --git a/__tests__/html2/basic/longURL.html b/__tests__/html2/basic/longURL.html new file mode 100644 index 0000000000..60d9c7f15e --- /dev/null +++ b/__tests__/html2/basic/longURL.html @@ -0,0 +1,58 @@ + + + + + + + + + + + +
+ + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-break-word-1-snap.png b/__tests__/html2/basic/longURL.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-break-word-1-snap.png rename to __tests__/html2/basic/longURL.html.snap-1.png diff --git a/__tests__/html2/basic/longURL.keepAll.html b/__tests__/html2/basic/longURL.keepAll.html new file mode 100644 index 0000000000..1c4fa9bd7a --- /dev/null +++ b/__tests__/html2/basic/longURL.keepAll.html @@ -0,0 +1,14 @@ + + + + + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-keep-all-1-snap.png b/__tests__/html2/basic/longURL.keepAll.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-long-ur-ls-with-keep-all-1-snap.png rename to __tests__/html2/basic/longURL.keepAll.html.snap-1.png diff --git a/__tests__/html2/basic/thumbnailCard.wrapTitle.html b/__tests__/html2/basic/thumbnailCard.wrapTitle.html new file mode 100644 index 0000000000..692dd14bdc --- /dev/null +++ b/__tests__/html2/basic/thumbnailCard.wrapTitle.html @@ -0,0 +1,72 @@ + + + + + + + + + + + +
+ + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-thumbnail-card-with-a-long-title-and-rich-card-wrap-title-set-to-default-value-1-snap.png b/__tests__/html2/basic/thumbnailCard.wrapTitle.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-thumbnail-card-with-a-long-title-and-rich-card-wrap-title-set-to-default-value-1-snap.png rename to __tests__/html2/basic/thumbnailCard.wrapTitle.html.snap-1.png diff --git a/__tests__/html2/basic/thumbnailCard.wrapTitle.wrap.html b/__tests__/html2/basic/thumbnailCard.wrapTitle.wrap.html new file mode 100644 index 0000000000..d8859a748a --- /dev/null +++ b/__tests__/html2/basic/thumbnailCard.wrapTitle.wrap.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/basic-js-thumbnail-card-with-a-long-title-and-rich-card-wrap-title-set-to-true-1-snap.png b/__tests__/html2/basic/thumbnailCard.wrapTitle.wrap.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/basic-js-thumbnail-card-with-a-long-title-and-rich-card-wrap-title-set-to-true-1-snap.png rename to __tests__/html2/basic/thumbnailCard.wrapTitle.wrap.html.snap-1.png diff --git a/__tests__/html2/hooks/private/renderHook.js b/__tests__/html2/hooks/private/renderHook.js index 1593e69293..6cea82fb96 100644 --- a/__tests__/html2/hooks/private/renderHook.js +++ b/__tests__/html2/hooks/private/renderHook.js @@ -59,7 +59,10 @@ export default function renderHook( const render = ({ renderCallbackProps }) => { const element = document.querySelector('main'); - ReactDOM.render(wrapUiIfNeeded(React.createElement(TestComponent, renderCallbackProps), renderOptions.wrapper), element); + ReactDOM.render( + wrapUiIfNeeded(React.createElement(TestComponent, { renderCallbackProps }), renderOptions.wrapper), + element + ); return { rerender: render, unmount: () => ReactDOM.unmountComponentAtNode(element) }; }; @@ -70,7 +73,7 @@ export default function renderHook( ); function rerender(rerenderCallbackProps) { - return baseRerender(React.createElement(TestComponent, { renderCallbackProps: rerenderCallbackProps })); + return baseRerender({ renderCallbackProps: rerenderCallbackProps }); } return { result, rerender, unmount }; diff --git a/__tests__/html/hooks.useActiveTyping.variable.html b/__tests__/html2/hooks/useActiveTyping.variable.html similarity index 84% rename from __tests__/html/hooks.useActiveTyping.variable.html rename to __tests__/html2/hooks/useActiveTyping.variable.html index 51eb270eaa..82e3d39df2 100644 --- a/__tests__/html/hooks.useActiveTyping.variable.html +++ b/__tests__/html2/hooks/useActiveTyping.variable.html @@ -13,6 +13,7 @@
+ + + + + + +
+ + + + diff --git a/__tests__/html2/hooks/useSendTypingIndicator.propsSetToTrue.html b/__tests__/html2/hooks/useSendTypingIndicator.propsSetToTrue.html new file mode 100644 index 0000000000..ce93d67b76 --- /dev/null +++ b/__tests__/html2/hooks/useSendTypingIndicator.propsSetToTrue.html @@ -0,0 +1,53 @@ + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/hooks/useSendTypingIndicator.propsUnset.html b/__tests__/html2/hooks/useSendTypingIndicator.propsUnset.html new file mode 100644 index 0000000000..336e8997b2 --- /dev/null +++ b/__tests__/html2/hooks/useSendTypingIndicator.propsUnset.html @@ -0,0 +1,48 @@ + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html/typing/activityOrder.html b/__tests__/html2/livestream/activityOrder.html similarity index 92% rename from __tests__/html/typing/activityOrder.html rename to __tests__/html2/livestream/activityOrder.html index 6bff57fbd0..72c1fdcd29 100644 --- a/__tests__/html/typing/activityOrder.html +++ b/__tests__/html2/livestream/activityOrder.html @@ -2,7 +2,7 @@ - + @@ -11,15 +11,15 @@
- + + + diff --git a/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-1.png b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-1.png new file mode 100644 index 0000000000..832ffa2931 Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-1.png differ diff --git a/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-2.png b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-2.png new file mode 100644 index 0000000000..9e7cfe959e Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-2.png differ diff --git a/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-3.png b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-3.png new file mode 100644 index 0000000000..c82eff1d3d Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.carousel.html.snap-3.png differ diff --git a/__tests__/html2/livestream/attachmentWithoutText.html b/__tests__/html2/livestream/attachmentWithoutText.html new file mode 100644 index 0000000000..d9d25042f8 --- /dev/null +++ b/__tests__/html2/livestream/attachmentWithoutText.html @@ -0,0 +1,152 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/attachmentWithoutText.html.snap-1.png b/__tests__/html2/livestream/attachmentWithoutText.html.snap-1.png new file mode 100644 index 0000000000..832ffa2931 Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.html.snap-1.png differ diff --git a/__tests__/html2/livestream/attachmentWithoutText.html.snap-2.png b/__tests__/html2/livestream/attachmentWithoutText.html.snap-2.png new file mode 100644 index 0000000000..5038235bd6 Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.html.snap-2.png differ diff --git a/__tests__/html2/livestream/attachmentWithoutText.html.snap-3.png b/__tests__/html2/livestream/attachmentWithoutText.html.snap-3.png new file mode 100644 index 0000000000..4d9b5883e0 Binary files /dev/null and b/__tests__/html2/livestream/attachmentWithoutText.html.snap-3.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html b/__tests__/html2/livestream/backtrackToEmpty.html new file mode 100644 index 0000000000..92ccfa8667 --- /dev/null +++ b/__tests__/html2/livestream/backtrackToEmpty.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-1.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-1.png new file mode 100644 index 0000000000..6d67efda15 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-1.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-2.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-2.png new file mode 100644 index 0000000000..19281f356d Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-2.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-3.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-3.png new file mode 100644 index 0000000000..f4e2bb27b1 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-3.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-4.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-4.png new file mode 100644 index 0000000000..19281f356d Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-4.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-5.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-5.png new file mode 100644 index 0000000000..6d67efda15 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-5.png differ diff --git a/__tests__/html2/livestream/backtrackToEmpty.html.snap-6.png b/__tests__/html2/livestream/backtrackToEmpty.html.snap-6.png new file mode 100644 index 0000000000..1189fab1b8 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToEmpty.html.snap-6.png differ diff --git a/__tests__/html2/livestream/backtrackToTypingIndicator.html b/__tests__/html2/livestream/backtrackToTypingIndicator.html new file mode 100644 index 0000000000..5b589f3772 --- /dev/null +++ b/__tests__/html2/livestream/backtrackToTypingIndicator.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-1.png b/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-1.png new file mode 100644 index 0000000000..57a9b96ab2 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-1.png differ diff --git a/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-2.png b/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-2.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/backtrackToTypingIndicator.html.snap-2.png differ diff --git a/__tests__/html2/livestream/batchedActivities.html b/__tests__/html2/livestream/batchedActivities.html index 4be25c5c1d..d4e8f72e24 100644 --- a/__tests__/html2/livestream/batchedActivities.html +++ b/__tests__/html2/livestream/batchedActivities.html @@ -2,6 +2,7 @@ + @@ -15,7 +16,10 @@ const { React: { createElement }, ReactDOM: { render }, - WebChat: { ReactWebChat } + WebChat: { + Components: { ReactWebChat }, + decorator: { WebChatDecorator } + } } = window; // Imports in UMD fashion. const streamId = crypto.randomUUID(); @@ -63,17 +67,21 @@ } }); - const App = () => - createElement(ReactWebChat, { - directLine, - store, - styleOptions: { - typingAnimationBackgroundImage: `url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')` - } - }); - // WHEN: Web Chat is rendered with 2 activities in the store. - render(createElement(App), document.getElementById('webchat')); + render( + createElement( + WebChatDecorator, + {}, + createElement(ReactWebChat, { + directLine, + store, + styleOptions: { + typingAnimationBackgroundImage: `url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')` + } + }) + ), + document.getElementById('webchat') + ); await pageConditions.uiConnected(); diff --git a/__tests__/html2/livestream/batchedActivities.html.snap-1.png b/__tests__/html2/livestream/batchedActivities.html.snap-1.png index 2b96991cd0..707d1bbf27 100644 Binary files a/__tests__/html2/livestream/batchedActivities.html.snap-1.png and b/__tests__/html2/livestream/batchedActivities.html.snap-1.png differ diff --git a/__tests__/html/typing/chunk.html b/__tests__/html2/livestream/chunk.html similarity index 88% rename from __tests__/html/typing/chunk.html rename to __tests__/html2/livestream/chunk.html index 9c0d9040db..20aee0d85d 100644 --- a/__tests__/html/typing/chunk.html +++ b/__tests__/html2/livestream/chunk.html @@ -2,7 +2,7 @@ - + @@ -11,15 +11,15 @@
- + @@ -11,15 +11,15 @@
- + + + diff --git a/__tests__/html2/livestream/contentless.carouselLayout.html.snap-1.png b/__tests__/html2/livestream/contentless.carouselLayout.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/contentless.carouselLayout.html.snap-1.png differ diff --git a/__tests__/html2/livestream/contentless.html b/__tests__/html2/livestream/contentless.html new file mode 100644 index 0000000000..f850afed9b --- /dev/null +++ b/__tests__/html2/livestream/contentless.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/contentless.html.snap-1.png b/__tests__/html2/livestream/contentless.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/contentless.html.snap-1.png differ diff --git a/__tests__/html/typing/informative.html b/__tests__/html2/livestream/contentlessLivestreamForever.html similarity index 65% rename from __tests__/html/typing/informative.html rename to __tests__/html2/livestream/contentlessLivestreamForever.html index 351f13ca42..5247cfd17e 100644 --- a/__tests__/html/typing/informative.html +++ b/__tests__/html2/livestream/contentlessLivestreamForever.html @@ -10,7 +10,16 @@
- + diff --git a/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-1.png b/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-1.png differ diff --git a/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-2.png b/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-2.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/contentlessLivestreamForever.html.snap-2.png differ diff --git a/__tests__/html2/livestream/css/pauseAnimation.css b/__tests__/html2/livestream/css/pauseAnimation.css new file mode 100644 index 0000000000..e47cbe4153 --- /dev/null +++ b/__tests__/html2/livestream/css/pauseAnimation.css @@ -0,0 +1,5 @@ +#webchat .border-loader__loader, +#webchat .border-flair { + animation-delay: -1s !important; + animation-play-state: paused !important; +} diff --git a/__tests__/html2/livestream/informative.html b/__tests__/html2/livestream/informative.html new file mode 100644 index 0000000000..e1bf3ba066 --- /dev/null +++ b/__tests__/html2/livestream/informative.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/livestream/informative.html.snap-1.png b/__tests__/html2/livestream/informative.html.snap-1.png new file mode 100644 index 0000000000..efdae97a25 Binary files /dev/null and b/__tests__/html2/livestream/informative.html.snap-1.png differ diff --git a/__tests__/html/typing/layout.html b/__tests__/html2/livestream/layout.html similarity index 97% rename from __tests__/html/typing/layout.html rename to __tests__/html2/livestream/layout.html index f8e81ef639..c15f62b759 100644 --- a/__tests__/html/typing/layout.html +++ b/__tests__/html2/livestream/layout.html @@ -108,7 +108,7 @@ // THEN: Should show the informative message with blue bottom border. // THEN: Should not show typing indicator. - await host.snapshot(); + await host.snapshot('local'); // WHEN: Interim activity arrives. await directLine.emulateIncomingActivity({ @@ -137,7 +137,7 @@ // THEN: Should show the informative message with blue bottom border. // THEN: Should not show typing indicator. - await host.snapshot(); + await host.snapshot('local'); // WHEN: Final activity arrives. await directLine.emulateIncomingActivity({ @@ -165,7 +165,7 @@ // THEN: Should show the informative message with blue bottom border. // THEN: Should not show typing indicator. - await host.snapshot(); + await host.snapshot('local'); }); diff --git a/__tests__/html2/livestream/layout.html.snap-1.png b/__tests__/html2/livestream/layout.html.snap-1.png new file mode 100644 index 0000000000..b8277f10c1 Binary files /dev/null and b/__tests__/html2/livestream/layout.html.snap-1.png differ diff --git a/__tests__/html2/livestream/layout.html.snap-2.png b/__tests__/html2/livestream/layout.html.snap-2.png new file mode 100644 index 0000000000..da3fe3709a Binary files /dev/null and b/__tests__/html2/livestream/layout.html.snap-2.png differ diff --git a/__tests__/html2/livestream/layout.html.snap-3.png b/__tests__/html2/livestream/layout.html.snap-3.png new file mode 100644 index 0000000000..d1264ce946 Binary files /dev/null and b/__tests__/html2/livestream/layout.html.snap-3.png differ diff --git a/__tests__/html/typing/outOfOrder.html b/__tests__/html2/livestream/outOfOrder.html similarity index 91% rename from __tests__/html/typing/outOfOrder.html rename to __tests__/html2/livestream/outOfOrder.html index 49887bcead..3fd1032810 100644 --- a/__tests__/html/typing/outOfOrder.html +++ b/__tests__/html2/livestream/outOfOrder.html @@ -2,7 +2,7 @@ - + @@ -11,15 +11,15 @@
- + @@ -11,15 +11,15 @@
- + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-1.png b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-1.png differ diff --git a/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-2.png b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-2.png new file mode 100644 index 0000000000..57a9b96ab2 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-2.png differ diff --git a/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-3.png b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-3.png new file mode 100644 index 0000000000..11123790b7 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-3.png differ diff --git a/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-4.png b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-4.png new file mode 100644 index 0000000000..f57d0319ac Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenChunkAndEmpty.html.snap-4.png differ diff --git a/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html new file mode 100644 index 0000000000..14c8d3c34b --- /dev/null +++ b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-1.png b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-1.png differ diff --git a/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-2.png b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-2.png new file mode 100644 index 0000000000..57a9b96ab2 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-2.png differ diff --git a/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-3.png b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-3.png new file mode 100644 index 0000000000..f57d0319ac Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenLivestreamAndTypingIndicator.html.snap-3.png differ diff --git a/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html new file mode 100644 index 0000000000..09fcdc7299 --- /dev/null +++ b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-1.png b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-1.png new file mode 100644 index 0000000000..9415f63a05 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-1.png differ diff --git a/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-2.png b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-2.png new file mode 100644 index 0000000000..ef517bd036 Binary files /dev/null and b/__tests__/html2/livestream/raceBetweenTypingAndContentlessLivestream.html.snap-2.png differ diff --git a/__tests__/html2/livestream/regretWithEmptyMessage.html b/__tests__/html2/livestream/regretWithEmptyMessage.html new file mode 100644 index 0000000000..e4cfe927fb --- /dev/null +++ b/__tests__/html2/livestream/regretWithEmptyMessage.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-1.png b/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-1.png new file mode 100644 index 0000000000..57a9b96ab2 Binary files /dev/null and b/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-1.png differ diff --git a/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-2.png b/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-2.png new file mode 100644 index 0000000000..ef517bd036 Binary files /dev/null and b/__tests__/html2/livestream/regretWithEmptyMessage.html.snap-2.png differ diff --git a/__tests__/html/typing/simultaneous.html b/__tests__/html2/livestream/simultaneous.html similarity index 89% rename from __tests__/html/typing/simultaneous.html rename to __tests__/html2/livestream/simultaneous.html index 3513b92577..3d73a12a15 100644 --- a/__tests__/html/typing/simultaneous.html +++ b/__tests__/html2/livestream/simultaneous.html @@ -2,7 +2,7 @@ - + @@ -11,15 +11,22 @@
- + + + + diff --git a/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-1.png b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-1.png new file mode 100644 index 0000000000..450f505575 Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-1.png differ diff --git a/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-2.png b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-2.png new file mode 100644 index 0000000000..cf8b5135ed Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-2.png differ diff --git a/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-3.png b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-3.png new file mode 100644 index 0000000000..f57d0319ac Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.carouselLayout.html.snap-3.png differ diff --git a/__tests__/html2/livestream/withAttachment.html b/__tests__/html2/livestream/withAttachment.html new file mode 100644 index 0000000000..be5de27542 --- /dev/null +++ b/__tests__/html2/livestream/withAttachment.html @@ -0,0 +1,141 @@ + + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/livestream/withAttachment.html.snap-1.png b/__tests__/html2/livestream/withAttachment.html.snap-1.png new file mode 100644 index 0000000000..450f505575 Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.html.snap-1.png differ diff --git a/__tests__/html2/livestream/withAttachment.html.snap-2.png b/__tests__/html2/livestream/withAttachment.html.snap-2.png new file mode 100644 index 0000000000..d9f06371a6 Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.html.snap-2.png differ diff --git a/__tests__/html2/livestream/withAttachment.html.snap-3.png b/__tests__/html2/livestream/withAttachment.html.snap-3.png new file mode 100644 index 0000000000..f57d0319ac Binary files /dev/null and b/__tests__/html2/livestream/withAttachment.html.snap-3.png differ diff --git a/__tests__/html2/typing/changeTypingAnimationDuration.html b/__tests__/html2/typing/changeTypingAnimationDuration.html new file mode 100644 index 0000000000..6745303cd1 --- /dev/null +++ b/__tests__/html2/typing/changeTypingAnimationDuration.html @@ -0,0 +1,99 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-1-snap.png b/__tests__/html2/typing/changeTypingAnimationDuration.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-1-snap.png rename to __tests__/html2/typing/changeTypingAnimationDuration.html.snap-1.png diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-2-snap.png b/__tests__/html2/typing/changeTypingAnimationDuration.html.snap-2.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-2-snap.png rename to __tests__/html2/typing/changeTypingAnimationDuration.html.snap-2.png diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-3-snap.png b/__tests__/html2/typing/changeTypingAnimationDuration.html.snap-3.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-changing-typing-indicator-duration-on-the-fly-3-snap.png rename to __tests__/html2/typing/changeTypingAnimationDuration.html.snap-3.png diff --git a/__tests__/html2/typing/noUserTypingIndicator.html b/__tests__/html2/typing/noUserTypingIndicator.html new file mode 100644 index 0000000000..a706b0106b --- /dev/null +++ b/__tests__/html2/typing/noUserTypingIndicator.html @@ -0,0 +1,63 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-should-not-show-typing-indicator-for-user-1-snap.png b/__tests__/html2/typing/noUserTypingIndicator.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-should-not-show-typing-indicator-for-user-1-snap.png rename to __tests__/html2/typing/noUserTypingIndicator.html.snap-1.png diff --git a/__tests__/html/typing/perActivityStyleOptions.html b/__tests__/html2/typing/perActivityStyleOptions.html similarity index 100% rename from __tests__/html/typing/perActivityStyleOptions.html rename to __tests__/html2/typing/perActivityStyleOptions.html diff --git a/__tests__/html2/typing/shouldHideTypingIndicator.html b/__tests__/html2/typing/shouldHideTypingIndicator.html new file mode 100644 index 0000000000..dd20e8358d --- /dev/null +++ b/__tests__/html2/typing/shouldHideTypingIndicator.html @@ -0,0 +1,96 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-not-display-after-second-activity-1-snap.png b/__tests__/html2/typing/shouldHideTypingIndicator.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-not-display-after-second-activity-1-snap.png rename to __tests__/html2/typing/shouldHideTypingIndicator.html.snap-1.png diff --git a/__tests__/html2/typing/shouldShowTypingIndicator.html b/__tests__/html2/typing/shouldShowTypingIndicator.html new file mode 100644 index 0000000000..eebc020320 --- /dev/null +++ b/__tests__/html2/typing/shouldShowTypingIndicator.html @@ -0,0 +1,74 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-display-in-send-box-1-snap.png b/__tests__/html2/typing/shouldShowTypingIndicator.html.snap-1.png similarity index 100% rename from __tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-display-in-send-box-1-snap.png rename to __tests__/html2/typing/shouldShowTypingIndicator.html.snap-1.png diff --git a/__tests__/html2/typing/simple.html b/__tests__/html2/typing/simple.html new file mode 100644 index 0000000000..705157afbe --- /dev/null +++ b/__tests__/html2/typing/simple.html @@ -0,0 +1,76 @@ + + + + + + + + + +
+ + + + diff --git a/__tests__/html/typing/typingIndicator.shouldNotRevive.html b/__tests__/html2/typing/typingIndicator.shouldNotRevive.html similarity index 100% rename from __tests__/html/typing/typingIndicator.shouldNotRevive.html rename to __tests__/html2/typing/typingIndicator.shouldNotRevive.html diff --git a/__tests__/sendTypingIndicator.js b/__tests__/sendTypingIndicator.js deleted file mode 100644 index 535c1365a2..0000000000 --- a/__tests__/sendTypingIndicator.js +++ /dev/null @@ -1,98 +0,0 @@ -import { By } from 'selenium-webdriver'; - -import { imageSnapshotOptions, timeouts } from './constants.json'; -import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown'; -import negationOf from './setup/conditions/negationOf'; -import typingActivityReceived from './setup/conditions/typingActivityReceived'; -import typingAnimationBackgroundImage from './setup/assets/typingIndicator'; -import typingIndicatorShown from './setup/conditions/typingIndicatorShown'; -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('Send typing indicator', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { sendTypingIndicator: true } }); - - await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true }); - - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - - const input = await driver.findElement(By.css('input[type="text"]')); - - await input.sendKeys('ABC'); - - // Typing indicator takes longer to come back - await driver.wait(typingActivityReceived(), timeouts.directLine); -}); - -test('typing indicator should display in SendBox', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions: { typingAnimationBackgroundImage } } }); - - await driver.wait(uiConnected(), timeouts.directLine); - - await pageObjects.sendMessageViaSendBox('typing 1', { waitForSend: true }); - - // Typing indicator takes longer to come back - await driver.wait(typingActivityReceived(), timeouts.directLine); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('typing indicator should not display after second activity', async () => { - const { driver, pageObjects } = await setupWebDriver({ - props: { - styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 10000 } - } - }); - - await pageObjects.sendMessageViaSendBox('typing', { waitForSend: true }); - await driver.wait(minNumActivitiesShown(3), timeouts.directLine); - - const base64PNG = await driver.takeScreenshot(); - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('changing typing indicator duration on-the-fly', async () => { - const { driver, pageObjects } = await setupWebDriver({ - props: { - styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 1000 } - } - }); - - await driver.wait(uiConnected(), timeouts.directLine); - - await pageObjects.sendMessageViaSendBox('typing 1', { waitForSend: true }); - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); - await driver.wait(typingIndicatorShown(), timeouts.ui); - - expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); - - await driver.wait(negationOf(typingIndicatorShown()), 2000); - - expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); - - await pageObjects.updateProps({ - styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 5000 } - }); - - await driver.wait(typingIndicatorShown(), timeouts.ui); - - expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); -}); - -test('should not show typing indicator for user', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { sendTypingIndicator: true } }); - - await driver.wait(uiConnected(), timeouts.directLine); - await pageObjects.typeInSendBox('Hello, World!'); - await driver.wait(negationOf(typingIndicatorShown()), 2000); - - const base64PNG = await driver.takeScreenshot(); - - expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); -}); diff --git a/docs/LIVESTREAMING.md b/docs/LIVESTREAMING.md index 211830dd87..7917c9df1d 100644 --- a/docs/LIVESTREAMING.md +++ b/docs/LIVESTREAMING.md @@ -46,6 +46,12 @@ For better UX, the informative message should be a prepared message with very lo Ephemeral message means the content should only be available for a limited time and should not be considered final. Interim activities are naturally ephemeral message. +### Concluded livestream + +> Also known as: finalized livestream. + +When a livestream has finished, it will be marked as closed and sealed against future updates. The said livestream is a concluded livestream. + ### Bot vs. copilot > This is _not_ an official statement from Microsoft. @@ -81,9 +87,9 @@ Then, send the following activity to start the livestream. Notes: -- `text` field is required but can be an empty string - - In this example, the bot is sending "A quick" as its being prepared by LLMs -- `type` field must be `typing` +- `text` field is required but can be an empty string + - In this example, the bot is sending "A quick" as its being prepared by LLMs +- `type` field must be `typing` After sending the activity, the bot must wait until the service will return the activity ID. This will be the session ID of the livestream. @@ -105,13 +111,13 @@ Subsequently, send the following interim activity. Notes: -- `channelData.streamId` field is the session ID, i.e. the activity ID of the first activity - - In this example, the first activity ID is assumed `"a-00001"` - - The session ID must be unique within the conversation -- `channelData.streamSequence` field should be incremented by 1 for every activity sent in the livestream -- `text` field should contains partial content from past interim activities - - `text` field in latter interim activities will replace `text` field in former interim activities - - Bot can use this capability to backtrack or erase response +- `channelData.streamId` field is the session ID, i.e. the activity ID of the first activity + - In this example, the first activity ID is assumed `"a-00001"` + - The session ID must be unique within the conversation +- `channelData.streamSequence` field should be incremented by 1 for every activity sent in the livestream +- `text` field should contains partial content from past interim activities + - `text` field in latter interim activities will replace `text` field in former interim activities + - Bot can use this capability to backtrack or erase response Bots can send as much interim activities as it needs. @@ -130,13 +136,13 @@ To conclude the livestream, send the following activity. Notes: -- `channelData.streamType` field is `final` -- `channelData.streamSequence` field should not be present, and assumed `Infinity` -- `text` field should contains the complete message -- `type` field must be `message` -- After the livestream has concluded, future activities for the livestream will be ignored -- This must not be the first activity in the livestream -- For best compatibility, do not send attachments or anything other than the `text` field +- `channelData.streamType` field is `final` +- `channelData.streamSequence` field should not be present, and assumed `Infinity` +- `text` field should contains the complete message +- `type` field must be `message` +- After the livestream has concluded, future activities for the livestream will be ignored +- This must not be the first activity in the livestream +- For best compatibility, do not send attachments or anything other than the `text` field ### Scenario 2: With informative message @@ -156,21 +162,71 @@ To send an [informative message](#informative-message), send the following activ Notes: -- `channelData.streamType` field is `informative` -- `text` field should describes how the bot is preparing the livestream -- `type` field must be `typing` -- The activity can be send as the first activity or interleaved with other interim activities - - Some clients may not show informative messages while interleaved with other interim activities - - For best compatibility, send informative messages before any other interim activities -- Latter informative messages will replace former informative messages +- `channelData.streamType` field is `informative` +- `text` field should describes how the bot is preparing the livestream +- `type` field must be `typing` +- The activity can be send as the first activity or interleaved with other interim activities + - Some clients may not show informative messages while interleaved with other interim activities + - For best compatibility, send informative messages before any other interim activities +- Latter informative messages will replace former informative messages + +### Scenario 3: Interim activities with no content + +> New since 2025-02-25. + +Traditionally, no bubbles will show for activities which do not have any text content or attachments, they are called contentless activity. For contentless activity in livestream, Web Chat will show typing indicator in lieu of message bubble. + +Contentless activities can appear in all phase of a livestream, including: start, middle, and end of the livestream. + +```json +{ + "channelData": { + "streamSequence": 1, + "streamType": "streaming" + }, + "type": "typing" +} +``` + +Notes: + +- `text` field can be either unset or an empty string +- `attachments` field is not set or is an empty array +- Web Chat will show a typing indicator + - The typing indicator will always appear until this livestream is concluded +- Only activity without `text` and `attachments` are considered contentless activity + - Activities filtered out by activity and attachment middleware are not considered contentless and will not show typing indicators + - This behavior may change in the future, middleware may be taken into account when considering an activity is contentless or not + +Final activity can be contentless. Upon the conclusion of the livestream with a contentless activity, message bubble related to the livestream will be removed. This is also called "regretting the livestream" and allows the bot to erase the response before concluding it. + +```json +{ + "channelData": { + "streamSequence": "a-00001", + "streamType": "final" + }, + "type": "typing" +} +``` + +Notes: + +- `text` field can be either unset or an empty string +- `type` should be `typing` + - In some systems, activities with `type` of `message` requires `text` field to also be set + - For best compatibility, we recommend setting the `type` to `typing` and `text` field unset +- If message bubble was shown, it will be removed + - Traditionally, no bubbles will show for contentless activity +- If typing indicator was shown, it will be removed ## Supportability End-to-end support of livestreaming relies on the following components: -- [Bot code](#bot-code-support) -- [Channel](#channel-support) -- [Client](#client-support) +- [Bot code](#bot-code-support) +- [Channel](#channel-support) +- [Client](#client-support) ### Bot code support @@ -182,23 +238,23 @@ If you already have a Bot Framework bot, most existing Bot SDK versions support Channel support depends on the following factors: -- Channel must support typing activity -- Channel must return activity ID of the sent activity -- Proactive messaging is optional but highly recommended - - Enabling proactive messaging will prevent client timeouts which may occur while the bot is generating the response +- Channel must support typing activity +- Channel must return activity ID of the sent activity +- Proactive messaging is optional but highly recommended + - Enabling proactive messaging will prevent client timeouts which may occur while the bot is generating the response Known channels which supports livestreaming: -- Direct Line (Web Socket) -- Teams +- Direct Line (Web Socket) +- Teams Known channels which does not support livestreaming: -- Direct Line (REST): ignores typing activity -- Direct Line ASE: does not return activity ID -- Direct Line Speech: does not return activity ID -- Email: ignores typing activity -- SMS: ignores typing activity +- Direct Line (REST): ignores typing activity +- Direct Line ASE: does not return activity ID +- Direct Line Speech: does not return activity ID +- Email: ignores typing activity +- SMS: ignores typing activity ### Client support @@ -210,29 +266,29 @@ Web Chat introduced livestreaming support since version [4.17.0](../CHANGELOG.md #### Background -- Assumption: interim activities can be sent as frequently as every 10 ms (100 Hz) -- ABS is a distributed system and may receive bot activity in an out-of-order fashion - - Every interim activities could send to a different HTTP endpoints - - In a distributed environment, the time receiving the HTTP request could differs +- Assumption: interim activities can be sent as frequently as every 10 ms (100 Hz) +- ABS is a distributed system and may receive bot activity in an out-of-order fashion + - Every interim activities could send to a different HTTP endpoints + - In a distributed environment, the time receiving the HTTP request could differs #### Solutions -- `channelData.streamSequence` will be used to identify obsoleted (outdated) activities -- Once the livestream has concluded, all future activities should be ignored +- `channelData.streamSequence` will be used to identify obsoleted (outdated) activities +- Once the livestream has concluded, all future activities should be ignored ### Packet loss or join after livestream started #### Background -- Client may join the conversation after livestream started -- Some services may drop typing activities as it has a lower quality-of-service (QoS) priority +- Client may join the conversation after livestream started +- Some services may drop typing activities as it has a lower quality-of-service (QoS) priority #### Solutions -- Content in interim activities should be overlapping - - Former interim activities will be obsoleted by latter interim activities - - The latest round of interim activities is sufficient to catchup the livestream -- Side benefits: bot can backtrack and erase response +- Content in interim activities should be overlapping + - Former interim activities will be obsoleted by latter interim activities + - The latest round of interim activities is sufficient to catchup the livestream +- Side benefits: bot can backtrack and erase response Bottomline: we understand the bandwidth usage could be large. But the benefits outweighted the shortcomings. Transports are free to implement their own mechanisms to save bandwidth. @@ -240,81 +296,98 @@ Bottomline: we understand the bandwidth usage could be large. But the benefits o #### Background -- Reduce/eliminate the need to update existing channel/transport/service - - 3P channel devs may have existing channel adapter that could be impacted by livestreaming -- Resource-heavy channels should not handle livestream - - Livestream should be ignored by SMS channel and Direct Line (REST) channel -- Unsupported channels should ignore livestream +- Reduce/eliminate the need to update existing channel/transport/service + - 3P channel devs may have existing channel adapter that could be impacted by livestreaming +- Resource-heavy channels should not handle livestream + - Livestream should be ignored by SMS channel and Direct Line (REST) channel +- Unsupported channels should ignore livestream #### Solutions -- Typing activity is being used to send interim activities - - According to [Direct Line specification](https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#typing-activity): "Typing activities represent ongoing input from a user or a bot." - - We are leveraging existing typing activity to send interims, channel/transport/service would not need an update -- Typing activity is naturally ignored by SMS, email, and other plain text channels -- Direct Line (REST) is a resource-heavy channel and livestreaming should be ignored - - Typing activity is naturally ignored by Direct Line (REST) channel to save resources -- Final activity in the livestream is sent as a normal message activity - - Channels that does not support livestreaming will be able to handle the final activity -- Side benefits: bot do not need a major update to use the livestreaming feature +- Typing activity is being used to send interim activities + - According to [Direct Line specification](https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#typing-activity): "Typing activities represent ongoing input from a user or a bot." + - We are leveraging existing typing activity to send interims, channel/transport/service would not need an update +- Typing activity is naturally ignored by SMS, email, and other plain text channels +- Direct Line (REST) is a resource-heavy channel and livestreaming should be ignored + - Typing activity is naturally ignored by Direct Line (REST) channel to save resources +- Final activity in the livestream is sent as a normal message activity + - Channels that does not support livestreaming will be able to handle the final activity +- Side benefits: bot do not need a major update to use the livestreaming feature ### Guaranteed start of livestream #### Background -- Some services requires a very clear signal to start a livestream -- Out-of-order delivery could affect this start of livestream signal +- Some services requires a very clear signal to start a livestream +- Out-of-order delivery could affect this start of livestream signal #### Solutions -- Bot would need to wait until the service replies with an activity ID - - The response from the service is a clear signal that the service has created a livestream -- Side benefits: the activity ID is an opaque string and can be used as the session ID +- Bot would need to wait until the service replies with an activity ID + - The response from the service is a clear signal that the service has created a livestream +- Side benefits: the activity ID is an opaque string and can be used as the session ID ### Storing of interim activities #### Background -- Some services may need to store every interim activities being sent - - Service implementation which concatenate interim activities in an out-of-order fashion could be very complex +- Some services may need to store every interim activities being sent + - Service implementation which concatenate interim activities in an out-of-order fashion could be very complex #### Solutions -- Interim activities will send overlapping content - - Services would not need to concatenate content itself +- Interim activities will send overlapping content + - Services would not need to concatenate content itself ### No replay #### Background -- Restoring chat history should not replay the livestreaming - - The final activity should be displayed instantly, interim activities should be skipped +- Restoring chat history should not replay the livestreaming + - The final activity should be displayed instantly, interim activities should be skipped #### Solutions -- Typing activity for all activities during a livestream - - Direct Line channel saves chat history without typing activities +- Typing activity for all activities during a livestream + - Direct Line channel saves chat history without typing activities ### Text format change #### Background -- Text format could change during interim activities - - This could cause layout to change rapidly and degrade UX +- Text format could change during interim activities + - This could cause layout to change rapidly and degrade UX #### Solutions -- Text format is assumed to be Markdown during the livestream +- Text format is assumed to be Markdown during the livestream ### Adding attachments during livestream #### Background -- Some clients may have difficulties handling attachments during livestream +- Some clients may have difficulties handling attachments during livestream #### Solutions -- As of this writing, no consensus has been reached on this issue - - "We don't want to show file attachments in interim activities." -- In the meanwhile, we are issuing best practices and discourage sending attachments in interim activities - - Bot should only send attachments in final activity +- As of this writing, no consensus has been reached on this issue +- For best compatibility, bot should only send attachments in final activity + +### Concluding the livestream without content + +#### Background + +- Some bots may regret that they opened a livestream and prefer to conclude it without any contents +- Some systems requires `text` to be set to a non-empty string for activities with `type` of `message` + +#### Solutions + +- To conclude a livestream without any contents, send the final message with `type` set to `typing`, with `text` either unset or set to an empty string + - For best compatibility, bot should not use `type` of `message` with empty `text` field + - Some systems cannot handle message activity without content + +## Frequently asked questions + +### Why an activity is not part of the livestream? + +Please verify the activity payload against the logic in [this file](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/core/src/utils/getActivityLivestreamingMetadata.ts). If the result is `undefined`, the activity is not part of a livestream. They could be missing required fields or failed some type validations. diff --git a/package-lock.json b/package-lock.json index 3cd753cda7..515579dbca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13099,6 +13099,18 @@ "url": "https://bevry.me/fund" } }, + "node_modules/iter-fest": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/iter-fest/-/iter-fest-0.2.1.tgz", + "integrity": "sha512-e6pj1pYuem1/UO/OkTBP7q9v7WMIRmw8fHT8Jld+099bGp8Hfq7Rg2g8A+esjPdj1JJ9vTvAPgIwJJj5frQe1A==", + "license": "MIT", + "dependencies": { + "iter-fest": "^0.2.1" + }, + "peerDependencies": { + "core-js-pure": "^3.37.1" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -24325,6 +24337,7 @@ "dependencies": { "botframework-webchat-core": "0.0.0-0", "globalize": "1.7.0", + "iter-fest": "^0.2.1", "math-random": "2.0.1", "prop-types": "15.8.1", "react-chain-of-responsibility": "0.2.0", diff --git a/packages/api/package.json b/packages/api/package.json index bfd78f1ae4..2b30b169fe 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -131,6 +131,7 @@ "dependencies": { "botframework-webchat-core": "0.0.0-0", "globalize": "1.7.0", + "iter-fest": "^0.2.1", "math-random": "2.0.1", "prop-types": "15.8.1", "react-chain-of-responsibility": "0.2.0", diff --git a/packages/api/src/decorator/private/ActivityDecorator.tsx b/packages/api/src/decorator/private/ActivityDecorator.tsx index 2662e45adf..ec28cf77f9 100644 --- a/packages/api/src/decorator/private/ActivityDecorator.tsx +++ b/packages/api/src/decorator/private/ActivityDecorator.tsx @@ -21,7 +21,9 @@ function ActivityDecorator({ activity, children }: Readonly<{ activity?: WebChat ? 'preparing' : type === 'interim activity' ? 'ongoing' - : undefined, + : type === 'contentless' + ? undefined // No bubble is shown for "contentless" livestream, should not decorate. + : undefined, from: supportedActivityRoles.includes(activity?.from?.role) ? activity?.from?.role : undefined }; }, [activity]); diff --git a/packages/api/src/hooks/private/reduceIterable.spec.ts b/packages/api/src/hooks/private/reduceIterable.spec.ts index 3e7b70a84f..bb92634a29 100644 --- a/packages/api/src/hooks/private/reduceIterable.spec.ts +++ b/packages/api/src/hooks/private/reduceIterable.spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-magic-numbers */ - import reduceIterable from './reduceIterable'; describe('when called with a summation reducer', () => { diff --git a/packages/api/src/providers/ActivityKeyer/ActivityKeyerComposer.tsx b/packages/api/src/providers/ActivityKeyer/ActivityKeyerComposer.tsx index dc74afed28..a6724c246f 100644 --- a/packages/api/src/providers/ActivityKeyer/ActivityKeyerComposer.tsx +++ b/packages/api/src/providers/ActivityKeyer/ActivityKeyerComposer.tsx @@ -1,4 +1,4 @@ -import type { WebChatActivity } from 'botframework-webchat-core'; +import { getActivityLivestreamingMetadata, type WebChatActivity } from 'botframework-webchat-core'; import React, { useCallback, useMemo, useRef, type ReactNode } from 'react'; import reduceIterable from '../../hooks/private/reduceIterable'; @@ -17,20 +17,6 @@ type ActivityToKeyMap = Map; type ClientActivityIdToKeyMap = Map; type KeyToActivitiesMap = Map; -function getTypingActivityId(activity: WebChatActivity): string | undefined { - const { type } = activity; - - if ( - (type === 'message' || type === 'typing') && - 'text' in activity && - typeof activity.text === 'string' && - 'streamId' in activity.channelData && - activity.channelData.streamId - ) { - return activity.channelData.streamId; - } -} - /** * React context composer component to assign a perma-key to every activity. * This will support both `useGetActivityByKey` and `useGetKeyByActivity` custom hooks. @@ -72,7 +58,7 @@ const ActivityKeyerComposer = ({ children }: Readonly<{ children?: ReactNode | u activities.forEach(activity => { const activityId = getActivityId(activity); const clientActivityId = getClientActivityId(activity); - const typingActivityId = getTypingActivityId(activity); + const typingActivityId = getActivityLivestreamingMetadata(activity)?.sessionId; const key = (clientActivityId && diff --git a/packages/api/src/providers/ActivityKeyer/private/someIterable.spec.ts b/packages/api/src/providers/ActivityKeyer/private/someIterable.spec.ts index 0bfc7187cf..124b6c4002 100644 --- a/packages/api/src/providers/ActivityKeyer/private/someIterable.spec.ts +++ b/packages/api/src/providers/ActivityKeyer/private/someIterable.spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-magic-numbers */ - import someIterable from './someIterable'; describe('when predicate return true should return true', () => { diff --git a/packages/api/src/providers/ActivityTyping/ActivityTypingComposer.tsx b/packages/api/src/providers/ActivityTyping/ActivityTypingComposer.tsx index e8408e0edb..709df6a7b1 100644 --- a/packages/api/src/providers/ActivityTyping/ActivityTypingComposer.tsx +++ b/packages/api/src/providers/ActivityTyping/ActivityTypingComposer.tsx @@ -1,73 +1,165 @@ -import { getActivityLivestreamingMetadata } from 'botframework-webchat-core'; -import React, { memo, useMemo, type ReactNode } from 'react'; -import { useRefFrom } from 'use-ref-from'; +import { getActivityLivestreamingMetadata, type WebChatActivity } from 'botframework-webchat-core'; +import { iteratorFind } from 'iter-fest'; +import React, { memo, useCallback, useMemo, type ReactNode } from 'react'; import numberWithInfinity from '../../hooks/private/numberWithInfinity'; -import useActivities from '../../hooks/useActivities'; import usePonyfill from '../../hooks/usePonyfill'; -import useUpsertedActivities from '../../providers/ActivityListener/useUpsertedActivities'; import ActivityTypingContext, { ActivityTypingContextType } from './private/Context'; -import useMemoWithPrevious from './private/useMemoWithPrevious'; +import useReduceActivities from './private/useReduceActivities'; import { type AllTyping } from './types/AllTyping'; -const INITIAL_ALL_TYPING_STATE = Object.freeze([Object.freeze(new Map())] as const); +type Entry = { + livestreamActivities: Map< + string, + { + activity: WebChatActivity; + contentful: boolean; + firstReceivedAt: number; + lastReceivedAt: number; + } + >; + name: string | undefined; + role: 'bot' | 'user'; + typingIndicator: + | { + activity: WebChatActivity; + duration: number; + firstReceivedAt: number; + lastReceivedAt: number; + } + | undefined; +}; type Props = Readonly<{ children?: ReactNode | undefined }>; const ActivityTypingComposer = ({ children }: Props) => { const [{ Date }] = usePonyfill(); - const [activities] = useActivities(); - const [upsertedActivities] = useUpsertedActivities(); - const activitiesRef = useRefFrom(activities); - - const allTypingState = useMemoWithPrevious]>( - (prevAllTypingState = INITIAL_ALL_TYPING_STATE) => { - const { current: activities } = activitiesRef; - const nextTyping = new Map(prevAllTypingState[0]); - let changed = false; - - const firstIndex = upsertedActivities.reduce( - (firstIndex, upsertedActivity) => Math.min(firstIndex, activities.indexOf(upsertedActivity)), - Infinity - ); - for (const activity of activities.slice(firstIndex)) { - const { - from, - from: { id, role }, - type - } = activity; - - const livestreamingMetadata = getActivityLivestreamingMetadata(activity); - - if (type === 'message' || livestreamingMetadata?.type === 'final activity') { - nextTyping.delete(id); - changed = true; - } else if (type === 'typing' && (role === 'bot' || role === 'user')) { - const currentTyping = nextTyping.get(id); - // TODO: When we rework on types of DLActivity, we will make sure all activities has "webChat.receivedAt", this coalesces can be removed. - const receivedAt = activity.channelData.webChat?.receivedAt || Date.now(); - - nextTyping.set(id, { - firstReceivedAt: currentTyping?.firstReceivedAt || receivedAt, - lastActivityDuration: numberWithInfinity( - activity.channelData.webChat?.styleOptions?.typingAnimationDuration - ), - lastReceivedAt: receivedAt, - name: from.name, - role, - type: livestreamingMetadata ? 'livestream' : 'busy' - }); - - changed = true; + const reducer = useCallback( + ( + prevTypingState: ReadonlyMap> | undefined, + activity: WebChatActivity + ): ReadonlyMap> | undefined => { + const { + from: { id, name, role }, + type + } = activity; + + if (role === 'channel') { + return prevTypingState; + } + + // A normal message activity, or final activity (which could be "message" or "typing"), will remove the typing indicator. + const receivedAt = activity.channelData?.webChat?.receivedAt || Date.now(); + + const livestreamingMetadata = getActivityLivestreamingMetadata(activity); + const typingState = new Map(prevTypingState); + const existingEntry = typingState.get(id); + const mutableEntry: Entry = { + typingIndicator: undefined, + ...existingEntry, + livestreamActivities: new Map(existingEntry?.livestreamActivities), + name, + role + }; + + if (livestreamingMetadata) { + mutableEntry.typingIndicator = undefined; + + const { sessionId } = livestreamingMetadata; + + if (livestreamingMetadata.type === 'final activity') { + mutableEntry.livestreamActivities.delete(sessionId); + } else { + mutableEntry.livestreamActivities.set( + sessionId, + Object.freeze({ + firstReceivedAt: Date.now(), + ...mutableEntry.livestreamActivities.get(sessionId), + activity, + contentful: livestreamingMetadata.type !== 'contentless', + lastReceivedAt: receivedAt + }) + ); } + } else if (type === 'message') { + mutableEntry.typingIndicator = undefined; + } else if (type === 'typing') { + mutableEntry.typingIndicator = Object.freeze({ + activity, + duration: numberWithInfinity(activity.channelData.webChat?.styleOptions?.typingAnimationDuration), + firstReceivedAt: mutableEntry.typingIndicator?.firstReceivedAt || Date.now(), + lastReceivedAt: receivedAt + }); } - return changed ? Object.freeze([nextTyping]) : prevAllTypingState; + typingState.set(id, Object.freeze(mutableEntry)); + + return Object.freeze(typingState); }, - [activitiesRef, upsertedActivities] + [Date] ); + const state: ReadonlyMap = useReduceActivities(reducer) || Object.freeze(new Map()); + + const allTyping = useMemo(() => { + const map = new Map(); + + for (const [id, entry] of state.entries()) { + const firstContentfulLivestream = iteratorFind( + entry.livestreamActivities.values(), + ({ contentful }) => contentful + ); + + const firstContentlessLivestream = iteratorFind( + entry.livestreamActivities.values(), + ({ contentful }) => !contentful + ); + + if (firstContentfulLivestream) { + map.set( + id, + Object.freeze({ + firstReceivedAt: firstContentfulLivestream.firstReceivedAt, + lastActivityDuration: Infinity, + lastReceivedAt: firstContentfulLivestream.lastReceivedAt, + name: entry.name, + role: entry.role, + type: 'livestream' + } satisfies AllTyping) + ); + } else if (firstContentlessLivestream) { + map.set( + id, + Object.freeze({ + firstReceivedAt: firstContentlessLivestream.firstReceivedAt, + lastActivityDuration: Infinity, + lastReceivedAt: firstContentlessLivestream.lastReceivedAt, + name: entry.name, + role: entry.role, + type: 'busy' + } satisfies AllTyping) + ); + } else if (entry.typingIndicator) { + map.set( + id, + Object.freeze({ + firstReceivedAt: entry.typingIndicator.firstReceivedAt, + lastActivityDuration: entry.typingIndicator.duration, + lastReceivedAt: entry.typingIndicator.lastReceivedAt, + name: entry.name, + role: entry.role, + type: 'busy' + } satisfies AllTyping) + ); + } + } + + return map; + }, [state]); + + const allTypingState = useMemo(() => Object.freeze([allTyping] as const), [allTyping]); + const context = useMemo(() => ({ allTypingState }), [allTypingState]); return {children}; diff --git a/packages/api/src/providers/ActivityTyping/private/useReduceActivities.spec.tsx b/packages/api/src/providers/ActivityTyping/private/useReduceActivities.spec.tsx new file mode 100644 index 0000000000..22d47e8861 --- /dev/null +++ b/packages/api/src/providers/ActivityTyping/private/useReduceActivities.spec.tsx @@ -0,0 +1,231 @@ +/** @jest-environment @happy-dom/jest-environment */ + +import { render, type RenderResult } from '@testing-library/react'; +import type { WebChatActivity } from 'botframework-webchat-core'; +import React, { type ComponentType } from 'react'; +import { type useActivities as UseActivitiesType } from '../../../hooks'; +import type UseReduceActivitiesType from './useReduceActivities'; + +type UseReduceActivitiesFn = Parameters[0]; + +const ACTIVITY_TEMPLATE = { + channelData: { + 'webchat:sequence-id': 0, + 'webchat:send-status': undefined + }, + from: { id: 'bot', role: 'bot' }, + id: 'a-00001', + text: '', + timestamp: '2025-03-10T12:34:56.789Z', + type: 'message' +} satisfies WebChatActivity & { type: 'message' }; + +describe('setup', () => { + let HookApp: ComponentType<{ fn: UseReduceActivitiesFn }>; + let useActivities: jest.Mock, Parameters>; + let useReduceActivities: jest.Mock< + ReturnType, + Parameters + >; + let fn: jest.Mock, Parameters>; + let renderResult: RenderResult; + + beforeEach(() => { + jest.mock('../../../hooks', () => ({ __esModule: true, useActivities: jest.fn(() => [[]]) })); + + ({ useActivities } = require('../../../hooks')); + + useReduceActivities = jest.fn(require('./useReduceActivities').default); + + fn = jest.fn().mockImplementation((prevResult, activity) => ({ + maxText: prevResult?.maxText > activity['text'] ? prevResult?.maxText : activity['text'] + })); + + HookApp = ({ fn }) => { + useReduceActivities(fn); + + return null; + }; + }); + + test('reduce nothing', () => { + render(); + + expect(fn).toHaveBeenCalledTimes(0); + }); + + describe('when the first activity is received', () => { + let firstActivity: WebChatActivity; + + beforeEach(() => { + firstActivity = { ...ACTIVITY_TEMPLATE, id: 'a-00001', text: 'Aloha!' }; + + useActivities.mockImplementationOnce(() => [[firstActivity]]); + + renderResult = render(); + }); + + describe('fn() should have been called', () => { + test('once', () => expect(fn).toHaveBeenCalledTimes(1)); + test('with the activity', () => + expect(fn).toHaveBeenLastCalledWith(undefined, firstActivity, 0, expect.arrayContaining([]))); + }); + + test('return value should be derived from the first activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Aloha!' })); + + describe('when the second activity is received', () => { + let secondActivity: WebChatActivity; + + beforeEach(() => { + secondActivity = { ...ACTIVITY_TEMPLATE, id: 'a-00002', text: 'Hello, World!' }; + + useActivities.mockImplementationOnce(() => [[firstActivity, secondActivity]]); + + renderResult.rerender(); + }); + + describe('fn() should have been called', () => { + test('twice in total', () => expect(fn).toHaveBeenCalledTimes(2)); + test('with the second activity', () => + expect(fn).toHaveBeenLastCalledWith({ maxText: 'Aloha!' }, secondActivity, 1, expect.arrayContaining([]))); + + test('return value should be derived from the second activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Hello, World!' })); + }); + + describe('when the third activity is inserted between the first and second activity', () => { + let thirdActivity: WebChatActivity; + + beforeEach(() => { + thirdActivity = { ...ACTIVITY_TEMPLATE, id: 'a-00003', text: 'Morning!' }; + + useActivities.mockImplementationOnce(() => [[firstActivity, thirdActivity, secondActivity]]); + + renderResult.rerender(); + }); + + describe('fn() should have been called', () => { + // It should call 2 more times because the first one should be from cache. + test('4 times in total', () => expect(fn).toHaveBeenCalledTimes(4)); + + test('with the third activity on 3rd call', () => + expect(fn).toHaveBeenNthCalledWith(3, { maxText: 'Aloha!' }, thirdActivity, 1, expect.arrayContaining([]))); + test('with the second activity on 4th call', () => + expect(fn).toHaveBeenNthCalledWith( + 4, + { maxText: 'Morning!' }, + secondActivity, + 2, + expect.arrayContaining([]) + )); + }); + + test('return value should be derived from the third activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Morning!' })); + + describe('when all activities are removed', () => { + beforeEach(() => { + useActivities.mockImplementationOnce(() => [[]]); + + renderResult.rerender(); + }); + + test('should not call fn()', () => expect(fn).toHaveBeenCalledTimes(4)); + test('return value should be undefined', () => expect(useReduceActivities).toHaveLastReturnedWith(undefined)); + }); + + describe('when the third activity is being replaced', () => { + let fourthActivity: WebChatActivity; + + beforeEach(() => { + fourthActivity = { ...ACTIVITY_TEMPLATE, id: 'a-00004', text: 'Good morning!' }; + + useActivities.mockImplementationOnce(() => [[firstActivity, fourthActivity, secondActivity]]); + + renderResult.rerender(); + }); + + describe('fn() should have been called', () => { + // It should call 2 more times because the first one should be from cache. + test('6 times in total', () => expect(fn).toHaveBeenCalledTimes(6)); + + test('with the fourth activity on 5rd call', () => + expect(fn).toHaveBeenNthCalledWith( + 5, + { maxText: 'Aloha!' }, + fourthActivity, + 1, + expect.arrayContaining([]) + )); + + test('with the second activity on 6th call', () => + expect(fn).toHaveBeenNthCalledWith( + 6, + { maxText: 'Good morning!' }, + secondActivity, + 2, + expect.arrayContaining([]) + )); + + test('return value should be derived from the second activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Hello, World!' })); + }); + }); + }); + + describe('when the first activity is removed', () => { + beforeEach(() => { + useActivities.mockImplementationOnce(() => [[secondActivity]]); + + renderResult.rerender(); + }); + + describe('should call fn', () => { + test('3 times in total', () => expect(fn).toHaveBeenCalledTimes(3)); + test('with the second activity only', () => + expect(fn).toHaveBeenLastCalledWith(undefined, secondActivity, 0, expect.arrayContaining([]))); + }); + + test('return value should be derived from the second activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Hello, World!' })); + }); + + describe('when the second activity is removed', () => { + beforeEach(() => { + useActivities.mockImplementationOnce(() => [[firstActivity]]); + + renderResult.rerender(); + }); + + describe('should not call fn()', () => test('once', () => expect(fn).toHaveBeenCalledTimes(2))); + + test('return value should be derived from the first activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Aloha!' })); + }); + }); + + describe('when all activities are removed', () => { + beforeEach(() => { + useActivities.mockImplementationOnce(() => [[]]); + + renderResult.rerender(); + }); + + test('should not call fn()', () => expect(fn).toHaveBeenCalledTimes(1)); + test('return value should be undefined', () => expect(useReduceActivities).toHaveLastReturnedWith(undefined)); + }); + + describe('when activities are unchanged', () => { + beforeEach(() => { + useActivities.mockImplementationOnce(() => [[firstActivity]]); + + renderResult.rerender(); + }); + + test('should not call fn()', () => expect(fn).toHaveBeenCalledTimes(1)); + test('return value should be derived from the first activity', () => + expect(useReduceActivities).toHaveLastReturnedWith({ maxText: 'Aloha!' })); + }); + }); +}); diff --git a/packages/api/src/providers/ActivityTyping/private/useReduceActivities.ts b/packages/api/src/providers/ActivityTyping/private/useReduceActivities.ts new file mode 100644 index 0000000000..290c223332 --- /dev/null +++ b/packages/api/src/providers/ActivityTyping/private/useReduceActivities.ts @@ -0,0 +1,48 @@ +import { type WebChatActivity } from 'botframework-webchat-core'; +import { useActivities } from '../../../hooks'; +import useMemoWithPrevious from './useMemoWithPrevious'; + +type Entry = Readonly<{ + activity: WebChatActivity; + value: T | undefined; +}>; + +export default function useReduceActivities( + fn: (prevValue: T, activity: WebChatActivity, index: number, activities: Readonly) => T +): T { + const [activities] = useActivities(); + + const state = useMemoWithPrevious[]>( + (state = Object.freeze([])) => { + let changed = activities.length !== state.length; + let prevValue: T | undefined; + let shouldRecompute = false; + + const nextState = activities.map>((activity, index) => { + const entry = state[+index]; + + if (!shouldRecompute && Object.is(entry?.activity, activity)) { + prevValue = entry?.value; + + // Skips the activity if it has been reduced in the past render loop. + return entry; + } + + changed = true; + shouldRecompute = true; + + return Object.freeze({ + activity, + value: (prevValue = fn(prevValue, activity, index, activities)) + }); + }); + + // Returns the original array if nothing changed. + return changed ? nextState : state; + }, + [activities, fn] + ); + + // eslint-disable-next-line no-magic-numbers + return state.at(-1)?.value; +} diff --git a/packages/component/src/Activity/StackedLayout.tsx b/packages/component/src/Activity/StackedLayout.tsx index 66591c34ad..7f91b43b4b 100644 --- a/packages/component/src/Activity/StackedLayout.tsx +++ b/packages/component/src/Activity/StackedLayout.tsx @@ -8,9 +8,9 @@ import React, { memo } from 'react'; import ScreenReaderText from '../ScreenReaderText'; import isZeroOrPositive from '../Utils/isZeroOrPositive'; import textFormatToContentType from '../Utils/textFormatToContentType'; -import useStyleSet from '../hooks/useStyleSet'; import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import useUniqueId from '../hooks/internal/useUniqueId'; +import useStyleSet from '../hooks/useStyleSet'; import Bubble from './Bubble'; import type { RenderAttachment } from 'botframework-webchat-api'; @@ -95,14 +95,14 @@ const StackedLayout = ({ const { bubbleNubOffset, bubbleNubSize, bubbleFromUserNubOffset, bubbleFromUserNubSize } = styleOptions; - const isMessage = activity.type === 'message'; + const isMessageOrTyping = activity.type === 'message' || activity.type === 'typing'; - const attachments = (isMessage && activity.attachments) || []; + const attachments = (isMessageOrTyping && activity.attachments) || []; const fromUser = activity.from.role === 'user'; - const messageBackDisplayText: string = (isMessage && activity.channelData?.messageBack?.displayText) || ''; + const messageBackDisplayText: string = (isMessageOrTyping && activity.channelData?.messageBack?.displayText) || ''; const isLivestreaming = !!getActivityLivestreamingMetadata(activity); - const activityDisplayText = isMessage + const activityDisplayText = isMessageOrTyping ? messageBackDisplayText || activity.text : isLivestreaming && 'text' in activity ? activity.text diff --git a/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx b/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx index ca3e01002b..8736bb73a5 100644 --- a/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx +++ b/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx @@ -1,7 +1,7 @@ import { ActivityMiddleware } from 'botframework-webchat-api'; +import { getActivityLivestreamingMetadata } from 'botframework-webchat-core'; import React from 'react'; -import { getActivityLivestreamingMetadata } from 'botframework-webchat-core'; import CarouselLayout from '../../Activity/CarouselLayout'; import StackedLayout from '../../Activity/StackedLayout'; @@ -21,8 +21,10 @@ export default function createCoreMiddleware(): ActivityMiddleware[] { type === 'conversationUpdate' || type === 'event' || type === 'invoke' || - // Do not show typing indicator except when it is livestreaming session - (type === 'typing' && !getActivityLivestreamingMetadata(activity)) || + // Do not show content for contentless livestream interims, or finalized activity without content. + (type === 'typing' && + (getActivityLivestreamingMetadata(activity)?.type === 'contentless' || + !(activity['text'] || activity.attachments?.length > 0))) || (type === 'message' && // Do not show postback (activity.channelData?.postBack || @@ -33,11 +35,7 @@ export default function createCoreMiddleware(): ActivityMiddleware[] { ) { return false; } else if (type === 'message' || type === 'typing') { - if ( - type === 'message' && - (activity.attachments?.length || 0) > 1 && - activity.attachmentLayout === 'carousel' - ) { + if ((activity.attachments?.length || 0) > 1 && activity.attachmentLayout === 'carousel') { // The following line is not a React functional component, it's a render function called by useCreateActivityRenderer() hook. // The function signature need to be compatible with older version of activity middleware, which was: // diff --git a/packages/component/src/hooks/internal/useMemoized.spec.jsx b/packages/component/src/hooks/internal/useMemoized.spec.jsx index be9478cf76..8ae3b994a6 100644 --- a/packages/component/src/hooks/internal/useMemoized.spec.jsx +++ b/packages/component/src/hooks/internal/useMemoized.spec.jsx @@ -1,7 +1,7 @@ /** @jest-environment @happy-dom/jest-environment */ -/* eslint-disable react/prop-types */ -/* eslint-disable no-undef */ + /* eslint no-magic-numbers: "off" */ + import React from 'react'; import { render } from 'react-dom'; import { act } from 'react-dom/test-utils'; diff --git a/packages/core/src/types/WebChatActivity.ts b/packages/core/src/types/WebChatActivity.ts index b3d19bb8f7..4575569e7b 100644 --- a/packages/core/src/types/WebChatActivity.ts +++ b/packages/core/src/types/WebChatActivity.ts @@ -169,15 +169,20 @@ type MessageActivityEssence = { // https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#typing-activity type TypingActivityEssence = | { + attachmentLayout?: 'carousel' | 'stacked'; + attachments?: DirectLineAttachment[]; + text?: undefined; type: 'typing'; } | { + attachmentLayout?: 'carousel' | 'stacked'; + attachments?: DirectLineAttachment[]; channelData: { streamId?: string | undefined; streamSequence: number; - streamType: 'informative' | 'streaming'; + streamType: 'informative' | 'streaming' | 'final'; }; - text: string; + text?: string | undefined; type: 'typing'; }; diff --git a/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js b/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js index cc084c5300..d1c8071e25 100644 --- a/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js @@ -6,7 +6,6 @@ import dateToLocaleISOString from './dateToLocaleISOString'; test('formatting a time in Chatham Islands timezone', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 567)); const actual = dateToLocaleISOString(date); diff --git a/packages/core/src/utils/dateToLocaleISOString.japan.spec.js b/packages/core/src/utils/dateToLocaleISOString.japan.spec.js index a5174a33ab..6304e26739 100644 --- a/packages/core/src/utils/dateToLocaleISOString.japan.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.japan.spec.js @@ -6,7 +6,6 @@ import dateToLocaleISOString from './dateToLocaleISOString'; test('formatting a time in Japan timezone', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 567)); const actual = dateToLocaleISOString(date); diff --git a/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js b/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js index 0f783d687a..154b100758 100644 --- a/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js @@ -6,7 +6,6 @@ import dateToLocaleISOString from './dateToLocaleISOString'; test('formatting a time in Cananda, Newfoundland timezone', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 567)); const actual = dateToLocaleISOString(date); diff --git a/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js b/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js index 7bf3549774..78e7632967 100644 --- a/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js @@ -6,7 +6,6 @@ import dateToLocaleISOString from './dateToLocaleISOString'; test('formatting a time in Pacific Standard Time timezone', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 567)); const actual = dateToLocaleISOString(date); diff --git a/packages/core/src/utils/dateToLocaleISOString.utc.spec.js b/packages/core/src/utils/dateToLocaleISOString.utc.spec.js index 610f0b2d85..5e3615826f 100644 --- a/packages/core/src/utils/dateToLocaleISOString.utc.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.utc.spec.js @@ -6,7 +6,6 @@ import dateToLocaleISOString from './dateToLocaleISOString'; test('formatting a time in UTC timezone', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 567)); const actual = dateToLocaleISOString(date); @@ -14,7 +13,6 @@ test('formatting a time in UTC timezone', () => { }); test('formatting a time in UTC timezone with zero milliseconds', () => { - // eslint-disable-next-line no-magic-numbers const date = new Date(Date.UTC(2000, 0, 1, 0, 12, 34, 0)); const actual = dateToLocaleISOString(date); diff --git a/packages/core/src/utils/getActivityLivestreamingMetadata.spec.ts b/packages/core/src/utils/getActivityLivestreamingMetadata.spec.ts index 739b1cac50..4969bfdc76 100644 --- a/packages/core/src/utils/getActivityLivestreamingMetadata.spec.ts +++ b/packages/core/src/utils/getActivityLivestreamingMetadata.spec.ts @@ -13,7 +13,7 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])(' streamType: 'streaming' }, id: 'a-00002', - text: '', + text: 'Hello, World!', type: 'typing' } as any; }); @@ -43,7 +43,7 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])(' streamType: 'informative' }, id: 'a-00002', - text: '', + text: 'Hello, World!', type: 'typing' } as any; }); @@ -72,7 +72,7 @@ describe.each([['with "streamId"' as const], ['without "streamId"' as const]])(' streamType: 'final' }, id: 'a-00002', - text: '', + text: 'Hello, World!', type: 'message' } as any; }); @@ -105,7 +105,6 @@ test('activity with "streamType" of "streaming" without critical fields should r describe.each([ ['integer', 1, true], ['zero', 0, false], - // eslint-disable-next-line no-magic-numbers ['decimal', 1.234, false] ])('activity with %s "streamSequence" should return undefined', (_, streamSequence, isValid) => { const activity = { @@ -122,12 +121,33 @@ describe.each([ } }); -test('activity with "streamType" of "final" but "type" of "typing" should return undefined', () => +describe('"typing" activity with "streamType" of "final"', () => { + test('should return undefined if "text" field is defined', () => + expect( + getActivityLivestreamingMetadata({ + channelData: { streamId: 'a-00001', streamType: 'final' }, + id: 'a-00002', + text: 'Final "typing" activity, must not have "text".', + type: 'typing' + } as any) + ).toBeUndefined()); + + test('should return truthy if "text" field is not defined', () => + expect( + getActivityLivestreamingMetadata({ + channelData: { streamId: 'a-00001', streamType: 'final' }, + id: 'a-00002', + // Final activity can be "typing" if it does not have "text". + type: 'typing' + } as any) + ).toHaveProperty('type', 'final activity')); +}); + +test('activity with "streamType" of "streaming" without "content" should return type of "contentless"', () => expect( getActivityLivestreamingMetadata({ - channelData: { streamType: 'final' }, - text: '', - // Final activity must be "message", not "typing". + channelData: { streamSequence: 1, streamType: 'streaming' }, + id: 'a-00001', type: 'typing' } as any) - ).toBeUndefined()); + ).toHaveProperty('type', 'contentless')); diff --git a/packages/core/src/utils/getActivityLivestreamingMetadata.ts b/packages/core/src/utils/getActivityLivestreamingMetadata.ts index cfa40ae099..ae18142bdb 100644 --- a/packages/core/src/utils/getActivityLivestreamingMetadata.ts +++ b/packages/core/src/utils/getActivityLivestreamingMetadata.ts @@ -1,30 +1,82 @@ -import { integer, literal, minValue, number, object, optional, pipe, safeParse, string, union } from 'valibot'; +import { + any, + array, + integer, + literal, + minValue, + nonEmpty, + number, + object, + optional, + pipe, + safeParse, + string, + undefinedable, + union +} from 'valibot'; import { type WebChatActivity } from '../types/WebChatActivity'; +const EMPTY_ARRAY = Object.freeze([]); + const streamSequenceSchema = pipe(number(), integer(), minValue(1)); const livestreamingActivitySchema = union([ + // Interim. + object({ + attachments: optional(array(any()), EMPTY_ARRAY), + channelData: object({ + // "streamId" is optional for the very first activity in the session. + streamId: optional(undefinedable(string())), + streamSequence: streamSequenceSchema, + streamType: literal('streaming') + }), + id: string(), + // "text" is optional. If not set or empty, it presents a contentless activity. + text: optional(undefinedable(string())), + type: literal('typing') + }), + // Informative message. object({ + attachments: optional(array(any()), EMPTY_ARRAY), channelData: object({ // "streamId" is optional for the very first activity in the session. - streamId: optional(string()), + streamId: optional(undefinedable(string())), streamSequence: streamSequenceSchema, - streamType: union([literal('informative'), literal('streaming')]) + streamType: literal('informative') }), id: string(), + // Informative message must have "text". text: string(), type: literal('typing') }), + // Conclude with a message. object({ + attachments: optional(array(any()), EMPTY_ARRAY), channelData: object({ - // "streamId" is required for the final activity in the session. The final activity must not be the sole activity in the session. - streamId: string(), + // "streamId" is required for the final activity in the session. + // The final activity must not be the sole activity in the session. + streamId: pipe(string(), nonEmpty()), streamType: literal('final') }), id: string(), - text: string(), + // If "text" is empty, it represents "regretting" the livestream. + text: optional(undefinedable(string())), type: literal('message') + }), + // Conclude without a message. + object({ + attachments: optional(array(any()), EMPTY_ARRAY), + channelData: object({ + // "streamId" is required for the final activity in the session. + // The final activity must not be the sole activity in the session. + streamId: pipe(string(), nonEmpty()), + streamType: literal('final') + }), + id: string(), + // If "text" is not set or empty, it represents "regretting" the livestream. + text: optional(undefinedable(literal(''))), + type: literal('typing') }) ]); @@ -34,7 +86,8 @@ const livestreamingActivitySchema = union([ * - `sessionId` - ID of the livestreaming session * - `sequenceNumber` - sequence number of the activity * - `type` - * - `"interim activity"` - current response, could be empty, partial-from-start, or complete response. + * - `"contentless"` - ongoing but no content, should show indicator + * - `"interim activity"` - current response, could be partial-from-start, or complete response. * More activities are expected. Future interim activities always replace past interim activities, enable erasing or backtracking response. * - `"informative message"` - optional side-channel informative message describing the current response, e.g. "Searching your document library". * Always replace past informative messages. May interleave with interim activities. @@ -48,7 +101,7 @@ export default function getActivityLivestreamingMetadata(activity: WebChatActivi | Readonly<{ sessionId: string; sequenceNumber: number; - type: 'final activity' | 'informative message' | 'interim activity'; + type: 'contentless' | 'final activity' | 'informative message' | 'interim activity'; }> | undefined { const result = safeParse(livestreamingActivitySchema, activity); @@ -69,7 +122,11 @@ export default function getActivityLivestreamingMetadata(activity: WebChatActivi : { sequenceNumber: output.channelData.streamSequence, sessionId, - type: output.channelData.streamType === 'informative' ? 'informative message' : 'interim activity' + type: !(output.text || output.attachments?.length) + ? 'contentless' + : output.channelData.streamType === 'informative' + ? 'informative message' + : 'interim activity' } ); } diff --git a/packages/test/harness/src/common/marshal.spec.js b/packages/test/harness/src/common/marshal.spec.js index 1e224cb6fc..08595a8d08 100644 --- a/packages/test/harness/src/common/marshal.spec.js +++ b/packages/test/harness/src/common/marshal.spec.js @@ -44,7 +44,6 @@ describe('Marshalling value of', () => { }); test('array', () => { - // eslint-disable-next-line no-magic-numbers expect(marshal([true, ERROR, 123, null, 'string', undefined])).toMatchInlineSnapshot(` Array [ true, diff --git a/packages/test/harness/src/common/unmarshal.spec.js b/packages/test/harness/src/common/unmarshal.spec.js index 189442496a..a690516cb2 100644 --- a/packages/test/harness/src/common/unmarshal.spec.js +++ b/packages/test/harness/src/common/unmarshal.spec.js @@ -40,7 +40,6 @@ describe('Unmarshalling value of', () => { expect( unmarshal([ true, - // eslint-disable-next-line no-magic-numbers 123, null, 'string', diff --git a/packages/test/page-object/src/globals/pageElements/activities.js b/packages/test/page-object/src/globals/pageElements/activities.js index ddf400d559..888c8674c0 100644 --- a/packages/test/page-object/src/globals/pageElements/activities.js +++ b/packages/test/page-object/src/globals/pageElements/activities.js @@ -1,5 +1,5 @@ import transcript from './transcript'; export default function activities() { - return transcript().querySelectorAll('.webchat__basic-transcript__activity'); + return Object.freeze(Array.from(transcript()?.querySelectorAll('.webchat__basic-transcript__activity') || [])); } diff --git a/packages/test/page-object/src/globals/pageElements/activityAttachments.js b/packages/test/page-object/src/globals/pageElements/activityAttachments.js new file mode 100644 index 0000000000..0b8c8bdb7e --- /dev/null +++ b/packages/test/page-object/src/globals/pageElements/activityAttachments.js @@ -0,0 +1,9 @@ +import activities from './activities'; + +export default function getActivityAttachments() { + return Object.freeze( + activities().map(element => + Object.freeze(Array.from(element.querySelectorAll('[aria-roledescription="attachment"]'))) + ) + ); +} diff --git a/packages/test/page-object/src/globals/pageElements/index.js b/packages/test/page-object/src/globals/pageElements/index.js index 83ea7e8a8c..f4eec34f6b 100644 --- a/packages/test/page-object/src/globals/pageElements/index.js +++ b/packages/test/page-object/src/globals/pageElements/index.js @@ -1,6 +1,7 @@ import activeActivity from './activeActivity'; import activities from './activities'; import activityActiveDescendantLabels from './activityActiveDescendantLabels'; +import activityAttachments from './activityAttachments'; import activityContents from './activityContents'; import activityStatuses from './activityStatuses'; import byTestId from './byTestId'; @@ -27,6 +28,7 @@ export { activeActivity, activities, activityActiveDescendantLabels, + activityAttachments, activityContents, activityStatuses, byTestId, diff --git a/serve-test.json b/serve-test.json index f926ca3e2f..73e2081de7 100644 --- a/serve-test.json +++ b/serve-test.json @@ -114,6 +114,7 @@ "/**/*.ico", "/**/*.js", "/**/*.json", - "/**/*.map" + "/**/*.map", + "/**/*.png" ] }