/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/DataFrame/columns/ProgressColumn.test.ts
295 строк
10 KB
Lukas Masuch
Extend sprintf number formatting to support thousand separator (#13284)
02 фев 2026, 14:00
Не верифицирован
02 фев 2026, 14:00
a9923f2
Код
Авторство
О чём код?
/** * 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 { GridCellKind } from "@glideapps/glide-data-grid" import { RangeCellType } from "@glideapps/glide-data-grid-cells" import { Field, Float64, Int64 } from "apache-arrow" import { DataFrameCellType } from "~lib/dataframes/arrowTypeUtils" import { mockTheme } from "~lib/mocks/mockTheme" import ProgressColumn, { ProgressColumnParams } from "./ProgressColumn" import { BaseColumnProps, ErrorCell, isErrorCell } from "./utils" const PROGRESS_COLUMN_TEMPLATE = { id: "1", name: "progress_column", title: "Progress column", indexNumber: 0, isEditable: false, isHidden: false, isIndex: false, isPinned: false, isStretched: false, arrowType: { type: DataFrameCellType.DATA, arrowField: new Field("progress_column", new Float64(), true), pandasType: { field_name: "progress_column", name: "progress_column", pandas_type: "float64", numpy_type: "float64", metadata: null, }, }, } as BaseColumnProps function getProgressColumn( params?: ProgressColumnParams ): ReturnType<typeof ProgressColumn> { return ProgressColumn( { ...PROGRESS_COLUMN_TEMPLATE, columnTypeOptions: params, } as BaseColumnProps, mockTheme.emotion ) } describe("ProgressColumn", () => { it("creates a valid column instance", () => { const mockColumn = getProgressColumn() expect(mockColumn.kind).toEqual("progress") expect(mockColumn.title).toEqual(PROGRESS_COLUMN_TEMPLATE.title) expect(mockColumn.id).toEqual(PROGRESS_COLUMN_TEMPLATE.id) expect(mockColumn.sortMode).toEqual("smart") // Column should be readonly: expect(mockColumn.isEditable).toEqual(false) const mockCell = mockColumn.getCell(0.5) expect(mockCell.kind).toEqual(GridCellKind.Custom) expect((mockCell as RangeCellType).data?.value).toEqual(0.5) expect((mockCell as RangeCellType).data?.label).toEqual("50%") }) it("supports configuring min/max scale", () => { const mockColumn = getProgressColumn() const mockCell = mockColumn.getCell(50) // Default min/max scale is 0/1 so the value should be at the maximum expect((mockCell as RangeCellType).data?.value).toEqual(1) // Use a different scale const mockColumn1 = getProgressColumn({ min_value: -100, max_value: 100, }) const mockCell1 = mockColumn1.getCell(50) // The value fits into the scale, so don't do anything: expect((mockCell1 as RangeCellType).data?.value).toEqual(50) // Use a different scale const mockColumn2 = getProgressColumn({ min_value: 100, max_value: -100, }) const mockCell2 = mockColumn2.getCell(50) // min needs to be bigger than max, so this should be an error cell: expect(isErrorCell(mockCell2)).toEqual(true) // Use a different scale const mockColumn3 = getProgressColumn({ min_value: undefined, max_value: -100, }) const mockCell3 = mockColumn3.getCell(50) // min and max need to be defined, so this should be an error cell: expect(isErrorCell(mockCell3)).toEqual(true) }) it.each([ // Supports almost the same as toSafeNumber [null, null], [undefined, null], ["", null], [[], null], ["0.1", 0.1], [0.1234, 0.1234], ])( "supports number-compatible value (%p parsed as %p)", // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. (input: any, value: number | null) => { const mockColumn = getProgressColumn() const cell = mockColumn.getCell(input) expect(mockColumn.getCellValue(cell)).toEqual(value) } ) it.each([ ["foo"], [[]], ["foo"], [[1, 2]], ["123.124.123"], ["--123"], ["2,,2"], // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. ])("%p results in error cell", (input: any) => { const mockColumn = getProgressColumn() const cell = mockColumn.getCell(input) expect(isErrorCell(cell)).toEqual(true) }) it.each([ // This should support everything that is supported by formatNumber // So we are not testing all the cases here, just a few to make sure it works // All other cases are tested for formatNumber in utils.test.ts [10.123, "%d", "10"], [10.123, "%i", "10"], [10.123, "%u", "10"], [10.123, "%f", "10.123"], [10.123, "%g", "10.123"], [10, "$%.2f", "$10.00"], [10.126, "$%.2f", "$10.13"], [10.123, "%.2f€", "10.12€"], [10.126, "($%.2f)", "($10.13)"], [65, "%d years", "65 years"], [1234567898765432, "%d ⭐", "1234567898765432 ⭐"], [72.3, "%.1f%%", "72.3%"], [-5.678, "%.1f", "-5.7"], [0.12, "percent", "12%"], [1100, "compact", "1.1K"], [-1234.567, "accounting", "(1,234.57)"], [1000000, "bytes", "1MB"], [1123456, "bytes", "1.1MB"], [1234, "bytes", "1.2KB"], [-1234.567, "dollar", "-$1,234.57"], [-1234.567, "euro", "-€1,234.57"], [-1234.567, "yen", "-¥1,235"], [-1234.567, "localized", "-1,234.567"], [-1234.567, "plain", "-1234.567"], [-1234.567, "scientific", "-1.235E3"], [-1234.567, "engineering", "-1.235E3"], // Thousand separator formats [1000, "%,.0f", "1,000"], [25000.25, "$%,.2f", "$25,000.25"], [9876543210, "%,.0f", "9,876,543,210"], [1234567.89, "%'_,.2f", "1_234_567.89"], [1234567, "%_d", "1_234_567"], [1234567.89, "%_.2f", "1_234_567.89"], ])( "formats %p with sprintf format %p to %p", (input: number, format: string, displayValue: string) => { const mockColumn = getProgressColumn({ format, }) const cell = mockColumn.getCell(input) expect((cell as RangeCellType).data.label).toEqual(displayValue) } ) it("shows an error cell if the numeric value is too large", () => { const mockColumn = getProgressColumn() const unsafeCell = mockColumn.getCell("1234567898765432123") expect(isErrorCell(unsafeCell)).toEqual(true) expect((unsafeCell as ErrorCell)?.data).toEqual("1234567898765432123") expect((unsafeCell as ErrorCell)?.errorDetails).toEqual( "The value is larger than the maximum supported integer values in number columns (2^53)." ) }) it.each([ [10, "%d %d"], [1234.5678, "%+.2E"], [0.000123456, "%+.2E"], [-0.000123456, "%+.2E"], [255, "%#x"], [4096, "%#X"], [42, "% d"], ])( "cannot format %p using the sprintf format %p", (input: number, format: string) => { const mockColumn = getProgressColumn({ format, }) const cell = mockColumn.getCell(input) expect(isErrorCell(cell)).toEqual(true) } ) it("correctly formats float values to percentage", () => { const mockColumn = getProgressColumn() const mockCell = mockColumn.getCell(0.52356) expect((mockCell as RangeCellType).data?.min).toEqual(0) expect((mockCell as RangeCellType).data?.max).toEqual(1) expect((mockCell as RangeCellType).data?.step).toEqual(0.01) // Correctly formats float values to percentage: expect((mockCell as RangeCellType).data?.value).toEqual(0.52356) expect((mockCell as RangeCellType).data?.label).toEqual("52.36%") }) it("correctly formats int values to percentage", () => { const mockColumn = ProgressColumn( { ...PROGRESS_COLUMN_TEMPLATE, arrowType: { type: DataFrameCellType.DATA, arrowField: new Field("progress_column", new Int64(), true), pandasType: { field_name: "progress_column", name: "progress_column", pandas_type: "int64", numpy_type: "int64", metadata: null, }, }, } as BaseColumnProps, mockTheme.emotion ) const mockCell = mockColumn.getCell(52) expect((mockCell as RangeCellType).data?.min).toEqual(0) expect((mockCell as RangeCellType).data?.max).toEqual(100) expect((mockCell as RangeCellType).data?.step).toEqual(1) // Correctly formats int values to percentage: expect((mockCell as RangeCellType).data?.value).toEqual(52) expect((mockCell as RangeCellType).data?.label).toEqual(" 52%") }) it("supports named color mapping and custom colors", () => { const blueColumn = getProgressColumn({ color: "blue" }) const blueCell = blueColumn.getCell(0.5) as RangeCellType expect(blueCell.data?.color).toEqual(mockTheme.emotion.colors.blueColor) const greyColumn = getProgressColumn({ color: "grey" }) const greyCell = greyColumn.getCell(0.5) as RangeCellType expect(greyCell.data?.color).toEqual(mockTheme.emotion.colors.grayColor) const customColor = "#123456" const customColumn = getProgressColumn({ color: customColor }) const customCell = customColumn.getCell(0.5) as RangeCellType expect(customCell.data?.color).toEqual(customColor) }) it("applies auto color based on value", () => { const autoColumn = getProgressColumn({ color: "auto" }) const highCell = autoColumn.getCell(0.75) as RangeCellType expect(highCell.data?.color).toEqual(mockTheme.emotion.colors.greenColor) const lowCell = autoColumn.getCell(0.25) as RangeCellType expect(lowCell.data?.color).toEqual(mockTheme.emotion.colors.redColor) }) it("applies auto-inverse color based on value", () => { const autoInvColumn = getProgressColumn({ color: "auto-inverse" }) const highCell = autoInvColumn.getCell(0.75) as RangeCellType expect(highCell.data?.color).toEqual(mockTheme.emotion.colors.redColor) const lowCell = autoInvColumn.getCell(0.25) as RangeCellType expect(lowCell.data?.color).toEqual(mockTheme.emotion.colors.greenColor) }) })