diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c97b42959..a39107585c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Legends: - Fixed the play, mute, and "more options" icons of audio players being invisible in dark high contrast themes, such as "Aquatic" on Windows, fixes [#5710](https://github.com/microsoft/BotFramework-WebChat/issues/5710), in PR [#5906](https://github.com/microsoft/BotFramework-WebChat/pull/5906), by [@OEvgeny](https://github.com/OEvgeny) - Fixed the "Positive" card action button having less than 4.5:1 color contrast, fixes [#5713](https://github.com/microsoft/BotFramework-WebChat/issues/5713), in PR [#5905](https://github.com/microsoft/BotFramework-WebChat/pull/5905), by [@OEvgeny](https://github.com/OEvgeny) - Fixed screen readers announcing a single audio, video, or animation in a card as a list item, such as "1 of 1", fixes [#5722](https://github.com/microsoft/BotFramework-WebChat/issues/5722), in PR [#5903](https://github.com/microsoft/BotFramework-WebChat/pull/5903), by [@OEvgeny](https://github.com/OEvgeny) +- Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny) ## [4.19.2] - 2026-10-08 diff --git a/__tests__/html2/attachment/video.url.html b/__tests__/html2/attachment/video.url.html new file mode 100644 index 0000000000..c81d738a29 --- /dev/null +++ b/__tests__/html2/attachment/video.url.html @@ -0,0 +1,218 @@ + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/attachment/video.url.html.snap-1.png b/__tests__/html2/attachment/video.url.html.snap-1.png new file mode 100644 index 0000000000..fb44bcf756 Binary files /dev/null and b/__tests__/html2/attachment/video.url.html.snap-1.png differ diff --git a/packages/component/src/Attachment/VideoContent.spec.tsx b/packages/component/src/Attachment/VideoContent.spec.tsx new file mode 100644 index 0000000000..e38b3ef7ec --- /dev/null +++ b/packages/component/src/Attachment/VideoContent.spec.tsx @@ -0,0 +1,34 @@ +/** @jest-environment @happy-dom/jest-environment */ +/// + +import { render } from '@testing-library/react'; +import React from 'react'; + +import VideoContent from './VideoContent'; + +// Production builds of `validateProps()` return props as-is, the URL is not validated. +jest.mock('@msinternal/botframework-webchat-react-valibot', () => ({ + ...jest.requireActual('@msinternal/botframework-webchat-react-valibot'), + validateProps: (_: unknown, props: unknown) => props +})); + +// The video player needs the Web Chat context, it is not under test. +jest.mock('../hooks/useStyleSet', () => () => [{ videoContent: '' }]); + +test.each([ + ['root-relative', '/videos/intro.mp4'], + ['path-relative', 'intro.mp4'], + ['protocol-relative', '//example.com/intro.mp4'], + ['without a host', 'https://'], + ['with an invalid host', 'http://[invalid'] +])('when the URL is %s in production build, should render nothing', (_, src) => { + const { container } = render(); + + expect(container.innerHTML).toBe(''); +}); + +test('when the URL is absolute in production build, should render a video player', () => { + const { container } = render(); + + expect(container.querySelector('video')?.getAttribute('src')).toBe('https://example.com/intro.mp4'); +}); diff --git a/packages/component/src/Attachment/VideoContent.tsx b/packages/component/src/Attachment/VideoContent.tsx index 35b9c7caff..5ec251db76 100644 --- a/packages/component/src/Attachment/VideoContent.tsx +++ b/packages/component/src/Attachment/VideoContent.tsx @@ -1,6 +1,7 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { onErrorResumeNext } from 'botframework-webchat-core'; import React, { memo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string, url, type InferInput } from 'valibot'; import HTMLVideoContent from './HTMLVideoContent'; import VimeoContent from './VimeoContent'; @@ -13,31 +14,14 @@ const YOUTUBE_WWW_SHORT_DOMAIN = 'www.youtu.be'; const VIMEO_DOMAIN = 'vimeo.com'; const VIMEO_WWW_DOMAIN = 'www.vimeo.com'; -// This is a workaround -// - Today, there is no good URL polyfill for older browser -// - Instead of writing a URL parser, for older browser, we will use this trick to parse the URL -function parseURL(url) { - let urlLike; - - if (typeof URL === 'function') { - urlLike = new URL(url); - } else { - urlLike = document.createElement('a'); - urlLike.setAttribute('href', url); - } - - const { hostname, pathname, search } = urlLike; - - return { hostname, pathname, search }; -} - const videoContentPropsSchema = pipe( object({ alt: optional(string()), autoPlay: optional(boolean()), loop: optional(boolean()), poster: optional(string()), - src: string() + // Relative URLs are not supported, they would point to a different server in each chat client. + src: pipe(string(), url()) }), readonly() ); @@ -47,26 +31,26 @@ type VideoContentProps = InferInput; function VideoContent(props: VideoContentProps) { const { alt, autoPlay = false, loop = false, poster, src } = validateProps(videoContentPropsSchema, props); - const { hostname, pathname, search } = parseURL(src); - const lastSegment = pathname.split('/').pop(); - const searchParams = new URLSearchParams(search); - - switch (hostname) { - case VIMEO_DOMAIN: - case VIMEO_WWW_DOMAIN: - return ; - - case YOUTUBE_DOMAIN: - case YOUTUBE_WWW_DOMAIN: - return ; - - case YOUTUBE_SHORT_DOMAIN: - case YOUTUBE_WWW_SHORT_DOMAIN: - return ; - - default: - return ; - } + const parsedURL = onErrorResumeNext(() => new URL(src)); + const hostname = parsedURL?.hostname; + const lastSegment = parsedURL?.pathname.split('/').pop(); + + // Embed YouTube and Vimeo videos only when the URL has a video ID, otherwise, play the URL in