/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/elements/ImageList/ImageList.test.tsx
370 строк
12 KB
Lukas Masuch
Remove deprecated and unused proto messages and fields (#13760)
30 янв 2026, 19:48
Не верифицирован
30 янв 2026, 19:48
85240a9
Код
Авторство
О чём код?
/** * 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 { ImageList as ImageListProto, streamlit } from "@streamlit/protobuf" import * as UseResizeObserver from "~lib/hooks/useResizeObserver" import { mockEndpoints } from "~lib/mocks/mocks" import { render, renderWithContexts } from "~lib/test_util" import ImageList, { ImageListProps } from "./ImageList" // 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("ImageList Element", () => { const buildMediaURL = vi.fn().mockReturnValue("https://mock.media.url") const sendClientErrorToHost = vi.fn() const getProps = ( elementProps: Partial<ImageListProto> = {}, widthConfig?: streamlit.IWidthConfig | null ): ImageListProps => ({ element: ImageListProto.create({ imgs: [ { caption: "a", url: "/media/mockImage1.jpeg" }, { caption: "b", url: "/media/mockImage2.jpeg" }, ], ...elementProps, }), widthConfig, endpoints: mockEndpoints({ buildMediaURL: buildMediaURL, sendClientErrorToHost: sendClientErrorToHost, }), }) beforeEach(() => { vi.spyOn(UseResizeObserver, "useResizeObserver").mockReturnValue({ elementRef: { current: null }, values: [250], }) }) it("renders without crashing", () => { const props = getProps() render(<ImageList {...props} />) expect(screen.getAllByRole("img")).toHaveLength(2) }) describe("New width configuration system", () => { it("renders explicit width for each image when using pixelWidth", () => { const props = getProps({}, { pixelWidth: 300 }) render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) images.forEach(image => { expect(image).toHaveStyle("width: 300px") }) }) it("uses stretch width behavior when useStretch is true", () => { const props = getProps({}, { useStretch: true }) render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) // When useStretch is true, width should match the element width (250px from mock) images.forEach(image => { expect(image).toHaveStyle("width: 250px") }) }) it("uses content width behavior when useContent is true", () => { const props = getProps({}, { useContent: true }) render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) // When useContent is true, width should be 100% (original size) images.forEach(image => { expect(image).toHaveStyle("width: 100%") }) }) }) describe("Fallback behavior", () => { it("defaults to content behavior when no widthConfig is provided", () => { const props = getProps() render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) images.forEach(image => { expect(image).toHaveStyle("width: 100%") }) }) it("defaults to content behavior when widthConfig is null", () => { const props = getProps({}, null) render(<ImageList {...props} />) const images = screen.getAllByRole("img") images.forEach(image => { expect(image).toHaveStyle("width: 100%") }) }) }) it("creates its `src` attribute using buildMediaURL", () => { const props = getProps() render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) expect(buildMediaURL).toHaveBeenNthCalledWith(1, "/media/mockImage1.jpeg") expect(buildMediaURL).toHaveBeenNthCalledWith(2, "/media/mockImage2.jpeg") images.forEach(image => { expect(image).toHaveAttribute("src", "https://mock.media.url") }) }) it("has a caption", () => { const props = getProps() render(<ImageList {...props} />) const captions = screen.getAllByTestId("stImageCaption") expect(captions).toHaveLength(2) expect(captions[0]).toHaveTextContent("a") expect(captions[1]).toHaveTextContent("b") }) it("renders explicit width for each caption when using pixelWidth", () => { const props = getProps({}, { pixelWidth: 300 }) render(<ImageList {...props} />) const captions = screen.getAllByTestId("stImageCaption") expect(captions).toHaveLength(2) captions.forEach(caption => { expect(caption).toHaveStyle("width: 300px") }) }) it("sends an CLIENT_ERROR message when the image source fails to load", () => { const props = getProps() render(<ImageList {...props} />) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) // Trigger the error event on the first image using fireEvent fireEvent.error(images[0]) // Verify the error was sent with correct parameters expect(sendClientErrorToHost).toHaveBeenCalledWith( "Image", "Image source failed to load", "onerror triggered", "https://mock.media.url/" ) }) 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", ({ resourceCrossOriginMode }) => { const props = getProps() renderWithContexts(<ImageList {...props} />, { libConfigContext: { resourceCrossOriginMode, }, }) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) images.forEach(image => { expect(image).not.toHaveAttribute("crossOrigin") }) } ) 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" as const, imgs: [ { caption: "a", url: "/media/image1.png" }, { caption: "b", url: "/media/image2.png" }, ], scenario: "relative URLs with anonymous mode", }, { expected: "use-credentials", resourceCrossOriginMode: "use-credentials" as const, imgs: [ { caption: "a", url: "/media/image1.png" }, { caption: "b", url: "/media/image2.png" }, ], scenario: "relative URLs with use-credentials mode", }, { expected: "anonymous", resourceCrossOriginMode: "anonymous" as const, imgs: [ { caption: "a", url: "https://backend.example.com:8080/media/image1.png", }, { caption: "b", url: "https://backend.example.com:8080/media/image2.png", }, ], scenario: "same origin as BACKEND_BASE_URL with anonymous mode", }, { expected: "use-credentials", resourceCrossOriginMode: "use-credentials" as const, imgs: [ { caption: "a", url: "https://backend.example.com:8080/media/image1.png", }, { caption: "b", url: "https://backend.example.com:8080/media/image2.png", }, ], scenario: "same origin as BACKEND_BASE_URL with use-credentials mode", }, ])( "sets crossOrigin to $expected when $scenario", ({ expected, resourceCrossOriginMode, imgs }) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const props = getProps({ imgs: imgs as any }) renderWithContexts(<ImageList {...props} />, { libConfigContext: { resourceCrossOriginMode, }, }) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) images.forEach(image => { expect(image).toHaveAttribute("crossOrigin", expected) }) } ) it.each([ { resourceCrossOriginMode: undefined, imgs: [ { caption: "a", url: "/media/image1.png" }, { caption: "b", url: "/media/image2.png" }, ], scenario: "relative URLs with undefined mode", }, { resourceCrossOriginMode: undefined, imgs: [ { caption: "a", url: "https://backend.example.com:8080/media/image1.png", }, { caption: "b", url: "https://backend.example.com:8080/media/image2.png", }, ], scenario: "same origin as BACKEND_BASE_URL with undefined mode", }, { resourceCrossOriginMode: "anonymous" as const, imgs: [ { caption: "a", url: "https://external.example.com/media/image1.png", }, { caption: "b", url: "https://external.example.com/media/image2.png", }, ], scenario: "different hostname than BACKEND_BASE_URL", }, { resourceCrossOriginMode: "anonymous" as const, imgs: [ { caption: "a", url: "https://backend.example.com:9000/media/image1.png", }, { caption: "b", url: "https://backend.example.com:9000/media/image2.png", }, ], scenario: "different port than BACKEND_BASE_URL", }, { resourceCrossOriginMode: "anonymous" as const, imgs: [ { caption: "a", url: "http://backend.example.com:8080/media/image1.png", }, { caption: "b", url: "http://backend.example.com:8080/media/image2.png", }, ], scenario: "different protocol than BACKEND_BASE_URL", }, ])( "does not set crossOrigin when $scenario", ({ resourceCrossOriginMode, imgs }) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const props = getProps({ imgs: imgs as any }) renderWithContexts(<ImageList {...props} />, { libConfigContext: { resourceCrossOriginMode, }, }) const images = screen.getAllByRole("img") expect(images).toHaveLength(2) images.forEach(image => { expect(image).not.toHaveAttribute("crossOrigin") }) } ) }) }) })