/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/elements/Video/Video.test.tsx
397 строк
13 KB
Ken McGrady
Update copyright header to 2026 (#13491)
02 янв 2026, 16:21
Не верифицирован
02 янв 2026, 16:21
374540a
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { fireEvent, screen } from "@testing-library/react" import { Video as VideoProto } from "@streamlit/protobuf" import * as UseResizeObserver from "~lib/hooks/useResizeObserver" import { mockEndpoints } from "~lib/mocks/mocks" import { render, renderWithContexts } from "~lib/test_util" import { WidgetStateManager as ElementStateManager } from "~lib/WidgetStateManager" import Video, { VideoProps } from "./Video" // Mock StreamlitConfig using global mock state (see vitest.setup.ts) vi.mock("@streamlit/utils", async () => { const actual = await vi.importActual("@streamlit/utils") return { ...actual, get StreamlitConfig() { return globalThis.__mockStreamlitConfig }, } }) describe("Video Element", () => { let buildMediaURL = vi.fn().mockReturnValue("https://mock.media.url") const sendClientErrorToHost = vi.fn() const mockSetElementState = vi.fn() const mockGetElementState = vi.fn() const elementMgrMock = { setElementState: mockSetElementState, getElementState: mockGetElementState, sendRerunBackMsg: vi.fn(), formsDataChanged: vi.fn(), } const getProps = (elementProps: Partial<VideoProto> = {}): VideoProps => ({ element: VideoProto.create({ url: "https://www.w3schools.com/html/mov_bbb.mp4", type: VideoProto.Type.UNUSED, startTime: 0, ...elementProps, }), endpoints: mockEndpoints({ buildMediaURL: buildMediaURL, sendClientErrorToHost: sendClientErrorToHost, }), elementMgr: elementMgrMock as unknown as ElementStateManager, }) beforeEach(() => { vi.clearAllMocks() vi.spyOn(UseResizeObserver, "useResizeObserver").mockReturnValue({ elementRef: { current: null }, values: [250], }) }) it("renders without crashing", async () => { const props = getProps() render(<Video {...props} />) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).toBeInTheDocument() expect(videoElement.classList).toContain("stVideo") }) it("has controls", async () => { const props = getProps() render(<Video {...props} />) expect(await screen.findByTestId("stVideo")).toHaveAttribute("controls") }) it("creates its `src` attribute using buildMediaURL", async () => { render(<Video {...getProps({ url: "/media/mockVideoFile.mp4" })} />) expect(buildMediaURL).toHaveBeenCalledWith("/media/mockVideoFile.mp4") expect(await screen.findByTestId("stVideo")).toHaveAttribute( "src", "https://mock.media.url" ) }) it("sends an CLIENT_ERROR message when the video source fails to load", () => { const props = getProps() render(<Video {...props} />) const videoElement = screen.getByTestId("stVideo") expect(videoElement).toBeInTheDocument() fireEvent.error(videoElement) expect(sendClientErrorToHost).toHaveBeenCalledWith( "Video", "Video source failed to load", "onerror triggered", "https://mock.media.url/" ) }) beforeEach(() => { vi.clearAllMocks() mockGetElementState.mockReturnValue(false) // By default, assume autoplay is not prevented }) it("does not autoplay if preventAutoplay is set", async () => { mockGetElementState.mockReturnValueOnce(true) // Autoplay should be prevented const props = getProps({ autoplay: true, id: "uniqueVideoId" }) render(<Video {...props} />) const audioElement = await screen.findByTestId("stVideo") expect(audioElement).not.toHaveAttribute("autoPlay") }) it("autoplays if preventAutoplay is not set and autoplay is true", async () => { mockGetElementState.mockReturnValueOnce(false) // Autoplay is not prevented const props = getProps({ autoplay: true, id: "uniqueVideoId" }) render(<Video {...props} />) const audioElement = await screen.findByTestId("stVideo") expect(audioElement).toHaveAttribute("autoPlay") }) it("calls setElementState to prevent future autoplay on first autoplay", () => { mockGetElementState.mockReturnValueOnce(false) // Autoplay is not prevented initially const props = getProps({ autoplay: true, id: "uniqueVideoId" }) render(<Video {...props} />) expect(mockSetElementState).toHaveBeenCalledTimes(1) expect(mockSetElementState).toHaveBeenCalledWith( props.element.id, "preventAutoplay", true ) }) // Test to ensure that setElementState is not called again if autoplay is already prevented it("does not call setElementState again if autoplay is already prevented", () => { mockGetElementState.mockReturnValueOnce(true) // Autoplay is already prevented const props = getProps({ autoplay: true, id: "uniqueVideoId" }) render(<Video {...props} />) expect(mockSetElementState).not.toHaveBeenCalled() }) describe("YouTube", () => { it("renders a youtube iframe", async () => { const props = getProps({ type: VideoProto.Type.YOUTUBE_IFRAME, }) render(<Video {...props} />) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).toBeInstanceOf(HTMLIFrameElement) expect(videoElement).toHaveAttribute( "src", "https://www.w3schools.com/html/mov_bbb.mp4" ) }) it("renders a youtube iframe with an starting time", async () => { const props = getProps({ type: VideoProto.Type.YOUTUBE_IFRAME, startTime: 10, }) render(<Video {...props} />) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).toBeInstanceOf(HTMLIFrameElement) expect(videoElement).toHaveAttribute( "src", "https://www.w3schools.com/html/mov_bbb.mp4?start=10" ) }) }) describe("updateTime", () => { const props = getProps() it("sets the current time to startTime on render", async () => { render(<Video {...props} />) const videoElement: HTMLMediaElement = await screen.findByTestId("stVideo") expect(videoElement.currentTime).toBe(0) }) it("updates the current time when startTime is changed", async () => { const { rerender } = render(<Video {...props} />) const videoElement: HTMLMediaElement = await screen.findByTestId("stVideo") expect(videoElement.currentTime).toBe(0) rerender(<Video {...getProps({ startTime: 10 })} />) expect(videoElement.currentTime).toBe(10) }) }) describe("subtitles", () => { it("renders subtitles properly", () => { const props = getProps({ subtitles: [ { url: "https://mock.subtitle.url" }, { url: "https://mock.subtitle.url2" }, ], }) props.endpoints.buildMediaURL = vi.fn(url => url) render(<Video {...props} />) // check that track elements are rendered const trackElements = screen.getAllByTestId("stVideoSubtitle") expect(trackElements).toHaveLength(2) // check that the track element has the correct src expect(trackElements[0]).toHaveAttribute( "src", "https://mock.subtitle.url" ) expect(trackElements[1]).toHaveAttribute( "src", "https://mock.subtitle.url2" ) }) it("checks the subtitles src url(s) on mount", () => { buildMediaURL = vi.fn(url => url) const props = getProps({ subtitles: [ { url: "https://mock.subtitle.url" }, { url: "https://mock.subtitle.url2" }, ], }) props.endpoints.buildMediaURL = buildMediaURL render(<Video {...props} />) expect(props.endpoints.checkSourceUrlResponse).toHaveBeenCalledTimes(2) expect(props.endpoints.checkSourceUrlResponse).toHaveBeenNthCalledWith( 1, "https://mock.subtitle.url", "Video Subtitle" ) expect(props.endpoints.checkSourceUrlResponse).toHaveBeenNthCalledWith( 2, "https://mock.subtitle.url2", "Video Subtitle" ) }) it("does not call checkSourceUrlResponse if there are no subtitles", () => { const props = getProps() render(<Video {...props} />) expect(props.endpoints.checkSourceUrlResponse).not.toHaveBeenCalled() }) }) describe("crossOrigin attribute", () => { it.each([ { resourceCrossOriginMode: "anonymous" }, { resourceCrossOriginMode: "use-credentials" }, { resourceCrossOriginMode: undefined }, ] as const)( "don't set crossOrigin attribute when StreamlitConfig.BACKEND_BASE_URL is not set", async ({ resourceCrossOriginMode }) => { const props = getProps({ url: "/media/mockVideoFile.mp4" }) renderWithContexts(<Video {...props} />, { libConfigContext: { resourceCrossOriginMode, }, }) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).not.toHaveAttribute("crossOrigin") } ) it("sets crossOrigin to 'anonymous' when in dev mode with subtitles regardless of resourceCrossOriginMode", async () => { // Store original NODE_ENV const originalNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = "development" const props = getProps({ subtitles: [{ url: "https://mock.subtitle.url" }], }) renderWithContexts(<Video {...props} />, { libConfigContext: { resourceCrossOriginMode: undefined, }, }) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).toHaveAttribute("crossOrigin", "anonymous") // Restore original NODE_ENV process.env.NODE_ENV = originalNodeEnv }) describe("with BACKEND_BASE_URL set", () => { beforeEach(() => { globalThis.__mockStreamlitConfig.BACKEND_BASE_URL = "https://backend.example.com:8080/app" }) afterEach(() => { globalThis.__mockStreamlitConfig = {} }) it.each([ { expected: "anonymous", resourceCrossOriginMode: "anonymous", url: "/media/video.mp4", scenario: "relative URL with anonymous mode", }, { expected: "use-credentials", resourceCrossOriginMode: "use-credentials", url: "/media/video.mp4", scenario: "relative URL with use-credentials mode", }, { expected: "anonymous", resourceCrossOriginMode: "anonymous" as const, url: "https://backend.example.com:8080/media/video.mp4", scenario: "same origin as BACKEND_BASE_URL with anonymous mode", }, { expected: "use-credentials", resourceCrossOriginMode: "use-credentials" as const, url: "https://backend.example.com:8080/media/video.mp4", scenario: "same origin as BACKEND_BASE_URL with use-credentials mode", }, ])( "sets crossOrigin to $expected when $scenario", async ({ expected, resourceCrossOriginMode, url }) => { const props = getProps({ url }) renderWithContexts(<Video {...props} />, { libConfigContext: { resourceCrossOriginMode: resourceCrossOriginMode as | "anonymous" | "use-credentials" | undefined, }, }) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).toHaveAttribute("crossOrigin", expected) } ) it.each([ { resourceCrossOriginMode: undefined, url: "/media/video.mp4", scenario: "relative URL with undefined mode", }, { resourceCrossOriginMode: undefined, url: "https://backend.example.com:8080/media/video.mp4", scenario: "same origin as BACKEND_BASE_URL with undefined mode", }, { resourceCrossOriginMode: "anonymous" as const, url: "https://external.example.com/media/video.mp4", scenario: "different hostname than BACKEND_BASE_URL", }, { resourceCrossOriginMode: "anonymous" as const, url: "https://backend.example.com:9000/media/video.mp4", scenario: "different port than BACKEND_BASE_URL", }, { resourceCrossOriginMode: "anonymous" as const, url: "http://backend.example.com:8080/media/video.mp4", scenario: "different protocol than BACKEND_BASE_URL", }, ])( "does not set crossOrigin when $scenario", async ({ resourceCrossOriginMode, url }) => { const props = getProps({ url }) renderWithContexts(<Video {...props} />, { libConfigContext: { resourceCrossOriginMode, }, }) const videoElement = await screen.findByTestId("stVideo") expect(videoElement).not.toHaveAttribute("crossOrigin") } ) }) }) })